From b7cc2fe223931373548936ded5659397d81cb9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sylwester=20Zieli=C5=84ski?= Date: Tue, 7 Sep 2021 14:48:31 +0200 Subject: [PATCH] Initial commit v3.0 --- .gitignore | 148 ++- app/.gitignore | 123 +- app/build.gradle | 105 +- app/libs/achartengine-1.2.0.jar | Bin 126046 -> 0 bytes app/proguard-rules.pro | 96 +- .../android/nrftoolbox/ApplicationTest.java | 35 - .../nrftoolbox/ExampleInstrumentedTest.kt | 24 + app/src/main/AndroidManifest.xml | 294 +---- .../android/nrftoolbox/AppHelpFragment.java | 79 -- .../android/nrftoolbox/FeaturesActivity.java | 223 ---- .../android/nrftoolbox/MainActivity.kt | 38 + .../nrftoolbox/SplashscreenActivity.java | 95 -- .../nrftoolbox/ToolboxApplication.java | 67 -- .../nrftoolbox/adapter/AppAdapter.java | 122 -- .../app/ExpandableListActivity.java | 302 ----- .../nrftoolbox/battery/BatteryManager.java | 126 -- .../battery/BatteryManagerCallbacks.java | 7 - .../android/nrftoolbox/bpm/BPMActivity.java | 179 --- .../android/nrftoolbox/bpm/BPMManager.java | 165 --- .../nrftoolbox/bpm/BPMManagerCallbacks.java | 31 - .../android/nrftoolbox/cgm/CGMManager.java | 454 ------- .../nrftoolbox/cgm/CGMManagerCallbacks.java | 48 - .../android/nrftoolbox/cgm/CGMRecord.java | 49 - .../nrftoolbox/cgm/CGMRecordsAdapter.java | 82 -- .../android/nrftoolbox/cgm/CGMSActivity.java | 282 ----- .../android/nrftoolbox/cgm/CGMService.java | 316 ----- .../android/nrftoolbox/csc/CSCActivity.java | 269 ----- .../android/nrftoolbox/csc/CSCManager.java | 132 --- .../nrftoolbox/csc/CSCManagerCallbacks.java | 28 - .../android/nrftoolbox/csc/CSCService.java | 238 ---- .../csc/settings/SettingsActivity.java | 56 - .../csc/settings/SettingsFragment.java | 79 -- .../android/nrftoolbox/dfu/DfuActivity.java | 843 ------------- .../nrftoolbox/dfu/DfuInitiatorActivity.java | 85 -- .../android/nrftoolbox/dfu/DfuService.java | 54 - .../nrftoolbox/dfu/NotificationActivity.java | 51 - .../dfu/adapter/FileBrowserAppsAdapter.java | 76 -- .../dfu/fragment/UploadCancelFragment.java | 86 -- .../dfu/fragment/ZipInfoFragment.java | 47 - .../dfu/settings/AboutDfuPreference.java | 59 - .../dfu/settings/SettingsActivity.java | 56 - .../dfu/settings/SettingsFragment.java | 112 -- .../gls/ExpandableRecordAdapter.java | 263 ----- .../nrftoolbox/gls/GlucoseActivity.java | 204 ---- .../nrftoolbox/gls/GlucoseManager.java | 434 ------- .../gls/GlucoseManagerCallbacks.java | 45 - .../android/nrftoolbox/gls/GlucoseRecord.java | 120 -- .../android/nrftoolbox/hr/HRActivity.java | 251 ---- .../android/nrftoolbox/hr/HRManager.java | 153 --- .../nrftoolbox/hr/HRManagerCallbacks.java | 30 - .../android/nrftoolbox/hr/LineGraphView.java | 123 -- .../android/nrftoolbox/ht/HTActivity.java | 225 ---- .../android/nrftoolbox/ht/HTManager.java | 111 -- .../nrftoolbox/ht/HTManagerCallbacks.java | 33 - .../android/nrftoolbox/ht/HTService.java | 235 ---- .../ht/settings/SettingsActivity.java | 56 - .../ht/settings/SettingsFragment.java | 41 - .../nrftoolbox/parser/AlertLevelParser.java | 54 - .../BloodPressureMeasurementParser.java | 92 -- .../parser/BodySensorLocationParser.java | 43 - .../parser/CGMMeasurementParser.java | 194 --- .../CGMSpecificOpsControlPointParser.java | 295 ----- .../parser/CSCMeasurementParser.java | 74 -- .../nrftoolbox/parser/DateTimeParser.java | 53 - .../GlucoseMeasurementContextParser.java | 188 --- .../parser/GlucoseMeasurementParser.java | 165 --- .../parser/HeartRateMeasurementParser.java | 111 -- .../IntermediateCuffPressureParser.java | 87 -- .../parser/RSCMeasurementParser.java | 74 -- .../RecordAccessControlPointParser.java | 172 --- .../parser/TemperatureMeasurementParser.java | 85 -- .../parser/TemperatureTypeParser.java | 59 - .../nrftoolbox/parser/TemplateParser.java | 59 - .../profile/BleProfileActivity.java | 441 ------- .../BleProfileExpandableListActivity.java | 443 ------- .../nrftoolbox/profile/BleProfileService.java | 609 ---------- .../BleProfileServiceReadyActivity.java | 673 ----------- .../profile/LoggableBleManager.java | 49 - .../BleMulticonnectProfileService.java | 636 ---------- ...lticonnectProfileServiceReadyActivity.java | 557 --------- .../profile/multiconnect/IDeviceLogger.java | 47 - .../nrftoolbox/proximity/DeviceAdapter.java | 151 --- .../proximity/LinkLossDialogFragment.java | 63 - .../proximity/ProximityActivity.java | 193 --- .../proximity/ProximityManager.java | 170 --- .../proximity/ProximityManagerCallbacks.java | 33 - .../proximity/ProximityServerManager.java | 66 -- .../proximity/ProximityService.java | 576 --------- .../android/nrftoolbox/rsc/RSCActivity.java | 290 ----- .../android/nrftoolbox/rsc/RSCManager.java | 103 -- .../nrftoolbox/rsc/RSCManagerCallbacks.java | 29 - .../android/nrftoolbox/rsc/RSCService.java | 285 ----- .../rsc/settings/SettingsActivity.java | 56 - .../rsc/settings/SettingsFragment.java | 42 - .../nrftoolbox/scanner/DeviceListAdapter.java | 212 ---- .../scanner/ExtendedBluetoothDevice.java | 55 - .../nrftoolbox/scanner/ScannerFragment.java | 292 ----- .../android/nrftoolbox/template/Readme.txt | 76 -- .../nrftoolbox/template/TemplateActivity.java | 191 --- .../nrftoolbox/template/TemplateManager.java | 234 ---- .../template/TemplateManagerCallbacks.java | 38 - .../nrftoolbox/template/TemplateService.java | 211 ---- .../TemplateCharacteristicCallback.java | 21 - .../callback/TemplateDataCallback.java | 47 - .../template/settings/SettingsActivity.java | 56 - .../template/settings/SettingsFragment.java | 41 - .../android/nrftoolbox/uart/UARTActivity.java | 855 -------------- .../nrftoolbox/uart/UARTButtonAdapter.java | 109 -- .../uart/UARTConfigurationsAdapter.java | 129 -- .../nrftoolbox/uart/UARTControlFragment.java | 135 --- .../nrftoolbox/uart/UARTEditDialog.java | 196 ---- .../nrftoolbox/uart/UARTInterface.java | 29 - .../uart/UARTLocalLogContentProvider.java | 39 - .../nrftoolbox/uart/UARTLogAdapter.java | 90 -- .../nrftoolbox/uart/UARTLogFragment.java | 299 ----- .../android/nrftoolbox/uart/UARTManager.java | 147 --- .../nrftoolbox/uart/UARTManagerCallbacks.java | 35 - .../UARTNewConfigurationDialogFragment.java | 138 --- .../android/nrftoolbox/uart/UARTService.java | 352 ------ .../uart/database/ConfigurationContract.java | 38 - .../uart/database/DatabaseHelper.java | 264 ----- .../nrftoolbox/uart/database/NameColumns.java | 28 - .../nrftoolbox/uart/database/UndoColumns.java | 27 - .../nrftoolbox/uart/domain/Command.java | 154 --- .../uart/domain/UartConfiguration.java | 71 -- .../UARTConfigurationSynchronizer.java | 140 --- .../android/nrftoolbox/ui/theme/Color.kt | 8 + .../android/nrftoolbox/ui/theme/Shape.kt | 11 + .../android/nrftoolbox/ui/theme/Theme.kt | 44 + .../android/nrftoolbox/ui/theme/Type.kt | 28 + .../nrftoolbox/utility/FileHelper.java | 66 -- .../nrftoolbox/utility/ParserUtils.java | 39 - .../wearable/MainWearableListenerService.java | 70 -- .../nrftoolbox/widget/ClosableSpinner.java | 36 - .../widget/DividerItemDecoration.java | 111 -- .../widget/ForegroundLinearLayout.java | 148 --- .../widget/ForegroundRelativeLayout.java | 148 --- app/src/main/res/animator/click_animator.xml | 44 - app/src/main/res/color/button_color.xml | 5 - app/src/main/res/color/menu_text.xml | 26 - app/src/main/res/drawable-hdpi/battery.png | Bin 392 -> 0 bytes .../res/drawable-hdpi/drawer_shadow.9.png | Bin 161 -> 0 bytes .../res/drawable-hdpi/ic_action_bluetooth.png | Bin 307 -> 0 bytes .../drawable-hdpi/ic_action_disconnect.png | Bin 332 -> 0 bytes app/src/main/res/drawable-hdpi/ic_help.png | Bin 1544 -> 0 bytes .../main/res/drawable-hdpi/ic_rssi_0_bar.png | Bin 1038 -> 0 bytes .../main/res/drawable-hdpi/ic_rssi_1_bar.png | Bin 1125 -> 0 bytes .../main/res/drawable-hdpi/ic_rssi_2_bars.png | Bin 1318 -> 0 bytes .../main/res/drawable-hdpi/ic_rssi_3_bars.png | Bin 1356 -> 0 bytes .../res/drawable-hdpi/ic_stat_notify_cgms.png | Bin 659 -> 0 bytes .../res/drawable-hdpi/ic_stat_notify_csc.png | Bin 872 -> 0 bytes .../res/drawable-hdpi/ic_stat_notify_hts.png | Bin 551 -> 0 bytes .../ic_stat_notify_proximity.png | Bin 782 -> 0 bytes .../res/drawable-hdpi/ic_stat_notify_rsc.png | Bin 502 -> 0 bytes .../drawable-hdpi/ic_stat_notify_template.png | Bin 546 -> 0 bytes .../res/drawable-hdpi/ic_stat_notify_uart.png | Bin 696 -> 0 bytes app/src/main/res/drawable-hdpi/shadow_l.png | Bin 110 -> 0 bytes app/src/main/res/drawable-hdpi/shadow_r.png | Bin 169 -> 0 bytes .../main/res/drawable-v21/ic_feature_bg.xml | 36 - .../ic_icon_button_background.xml | 18 - .../drawable-v21/uart_button_activated.xml | 32 - .../drawable-v21/uart_button_background.xml | 30 - .../res/drawable-v21/uart_button_normal.xml | 32 - .../drawable-v24/ic_launcher_foreground.xml | 30 + app/src/main/res/drawable-xhdpi/app_drive.png | Bin 2573 -> 0 bytes .../res/drawable-xhdpi/app_file_manager.png | Bin 3463 -> 0 bytes .../res/drawable-xhdpi/app_google_play.png | Bin 2247 -> 0 bytes .../drawable-xhdpi/app_total_commander.png | Bin 1596 -> 0 bytes .../btn_default_focused_holo_light.9.png | Bin 476 -> 0 bytes .../res/drawable-xhdpi/drawer_shadow.9.png | Bin 174 -> 0 bytes .../drawable-xhdpi/ic_action_bluetooth.png | Bin 344 -> 0 bytes .../drawable-xhdpi/ic_action_disconnect.png | Bin 392 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_help.png | Bin 1796 -> 0 bytes .../ic_nrf_connect_feature_fg.png | Bin 846 -> 0 bytes .../drawable-xhdpi/ic_stat_notify_cgms.png | Bin 844 -> 0 bytes .../res/drawable-xhdpi/ic_stat_notify_csc.png | Bin 1196 -> 0 bytes .../res/drawable-xhdpi/ic_stat_notify_hts.png | Bin 615 -> 0 bytes .../ic_stat_notify_proximity.png | Bin 928 -> 0 bytes .../res/drawable-xhdpi/ic_stat_notify_rsc.png | Bin 751 -> 0 bytes .../ic_stat_notify_template.png | Bin 658 -> 0 bytes .../drawable-xhdpi/ic_stat_notify_uart.png | Bin 922 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_1.png | Bin 493 -> 0 bytes .../res/drawable-xhdpi/ic_uart_1_small.png | Bin 355 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_2.png | Bin 1278 -> 0 bytes .../res/drawable-xhdpi/ic_uart_2_small.png | Bin 903 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_3.png | Bin 1344 -> 0 bytes .../res/drawable-xhdpi/ic_uart_3_small.png | Bin 968 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_4.png | Bin 742 -> 0 bytes .../res/drawable-xhdpi/ic_uart_4_small.png | Bin 559 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_5.png | Bin 982 -> 0 bytes .../res/drawable-xhdpi/ic_uart_5_small.png | Bin 735 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_6.png | Bin 1516 -> 0 bytes .../res/drawable-xhdpi/ic_uart_6_small.png | Bin 1021 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_7.png | Bin 901 -> 0 bytes .../res/drawable-xhdpi/ic_uart_7_small.png | Bin 630 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_8.png | Bin 1646 -> 0 bytes .../res/drawable-xhdpi/ic_uart_8_small.png | Bin 1181 -> 0 bytes app/src/main/res/drawable-xhdpi/ic_uart_9.png | Bin 1473 -> 0 bytes .../res/drawable-xhdpi/ic_uart_9_small.png | Bin 1015 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_about.png | Bin 1215 -> 0 bytes .../drawable-xhdpi/ic_uart_about_small.png | Bin 860 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_down.png | Bin 726 -> 0 bytes .../res/drawable-xhdpi/ic_uart_down_small.png | Bin 582 -> 0 bytes .../res/drawable-xhdpi/ic_uart_forward.png | Bin 648 -> 0 bytes .../drawable-xhdpi/ic_uart_forward_small.png | Bin 613 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_left.png | Bin 886 -> 0 bytes .../res/drawable-xhdpi/ic_uart_left_small.png | Bin 744 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_pause.png | Bin 297 -> 0 bytes .../drawable-xhdpi/ic_uart_pause_small.png | Bin 244 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_play.png | Bin 669 -> 0 bytes .../res/drawable-xhdpi/ic_uart_play_small.png | Bin 531 -> 0 bytes .../res/drawable-xhdpi/ic_uart_rewind.png | Bin 694 -> 0 bytes .../drawable-xhdpi/ic_uart_rewind_small.png | Bin 601 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_right.png | Bin 861 -> 0 bytes .../drawable-xhdpi/ic_uart_right_small.png | Bin 727 -> 0 bytes .../res/drawable-xhdpi/ic_uart_settings.png | Bin 1661 -> 0 bytes .../drawable-xhdpi/ic_uart_settings_small.png | Bin 1231 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_stop.png | Bin 262 -> 0 bytes .../res/drawable-xhdpi/ic_uart_stop_small.png | Bin 224 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_up.png | Bin 681 -> 0 bytes .../res/drawable-xhdpi/ic_uart_up_small.png | Bin 631 -> 0 bytes app/src/main/res/drawable-xhdpi/shadow_l.png | Bin 110 -> 0 bytes app/src/main/res/drawable-xhdpi/shadow_r.png | Bin 169 -> 0 bytes app/src/main/res/drawable-xhdpi/zip.png | Bin 86140 -> 0 bytes .../drawable-xxhdpi/action_bar_shadow.9.png | Bin 139 -> 0 bytes .../drawable-xxhdpi/ic_action_add_normal.png | Bin 1289 -> 0 bytes .../drawable-xxhdpi/ic_action_add_pressed.png | Bin 1520 -> 0 bytes .../drawable-xxhdpi/ic_action_bluetooth.png | Bin 502 -> 0 bytes .../ic_action_clear_normal.png | Bin 655 -> 0 bytes .../ic_action_clear_pressed.png | Bin 528 -> 0 bytes .../drawable-xxhdpi/ic_action_disconnect.png | Bin 512 -> 0 bytes .../ic_action_download_normal.png | Bin 329 -> 0 bytes .../ic_action_download_pressed.png | Bin 320 -> 0 bytes .../drawable-xxxhdpi/ic_action_bluetooth.png | Bin 595 -> 0 bytes .../drawable-xxxhdpi/ic_action_disconnect.png | Bin 642 -> 0 bytes .../res/drawable-xxxhdpi/ic_battery_alert.png | Bin 249 -> 0 bytes .../res/drawable-xxxhdpi/ic_battery_full.png | Bin 228 -> 0 bytes .../res/drawable-xxxhdpi/ic_bpm_feature.png | Bin 8685 -> 0 bytes .../res/drawable-xxxhdpi/ic_cgms_feature.png | Bin 11447 -> 0 bytes .../res/drawable-xxxhdpi/ic_csc_feature.png | Bin 9654 -> 0 bytes .../res/drawable-xxxhdpi/ic_dfu_feature.png | Bin 10698 -> 0 bytes .../drawable-xxxhdpi/ic_glucose_feature.png | Bin 10896 -> 0 bytes .../res/drawable-xxxhdpi/ic_hrs_feature.png | Bin 9037 -> 0 bytes .../res/drawable-xxxhdpi/ic_hts_feature.png | Bin 7235 -> 0 bytes .../drawable-xxxhdpi/ic_proximity_feature.png | Bin 10765 -> 0 bytes .../res/drawable-xxxhdpi/ic_rsc_feature.png | Bin 11880 -> 0 bytes .../ic_stat_notify_proximity_find.png | Bin 1451 -> 0 bytes .../ic_stat_notify_proximity_silent.png | Bin 1449 -> 0 bytes .../drawable-xxxhdpi/ic_template_feature.png | Bin 9210 -> 0 bytes .../res/drawable-xxxhdpi/ic_uart_feature.png | Bin 8602 -> 0 bytes .../main/res/drawable/app_file_browser.xml | 28 - app/src/main/res/drawable/ic_action_add.xml | 28 - app/src/main/res/drawable/ic_action_clear.xml | 28 - .../main/res/drawable/ic_action_download.xml | 28 - app/src/main/res/drawable/ic_battery.xml | 27 - app/src/main/res/drawable/ic_feature_bg.xml | 28 - app/src/main/res/drawable/ic_feature_bg_n.xml | 27 - app/src/main/res/drawable/ic_feature_bg_p.xml | 27 - .../main/res/drawable/ic_feature_small_bg.xml | 28 - .../res/drawable/ic_feature_small_bg_n.xml | 29 - .../res/drawable/ic_feature_small_bg_p.xml | 29 - .../res/drawable/ic_launcher_background.xml | 170 +++ .../drawable/ic_nrf_connect_feature_small.xml | 26 - app/src/main/res/drawable/ic_rssi_bar.xml | 28 - app/src/main/res/drawable/nordic_logo.xml | 142 --- .../res/drawable/nordic_logo_horiz_white.xml | 31 - app/src/main/res/drawable/start_edit_mode.xml | 28 - app/src/main/res/drawable/stop_edit_mode.xml | 28 - app/src/main/res/drawable/uart_button.xml | 44 - .../res/drawable/uart_button_background.xml | 32 - .../main/res/drawable/uart_button_small.xml | 44 - .../uart_dialog_button_background.xml | 30 - .../res/layout-land/activity_feature_bpm.xml | 298 ----- .../res/layout-land/activity_feature_cgms.xml | 220 ---- .../res/layout-land/activity_feature_csc.xml | 295 ----- .../res/layout-land/activity_feature_dfu.xml | 293 ----- .../res/layout-land/activity_feature_gls.xml | 222 ---- .../res/layout-land/activity_feature_hrs.xml | 153 --- .../res/layout-land/activity_feature_hts.xml | 112 -- .../activity_feature_proximity.xml | 90 -- .../res/layout-land/activity_feature_rsc.xml | 325 ----- .../res/layout-land/activity_feature_uart.xml | 67 -- .../res/layout-land/feature_uart_button.xml | 29 - .../fragment_feature_uart_control.xml | 95 -- .../activity_feature_bpm.xml | 299 ----- .../activity_feature_csc.xml | 257 ---- .../activity_feature_dfu.xml | 286 ----- .../activity_feature_hrs.xml | 154 --- .../feature_uart_button.xml | 28 - .../activity_feature_rsc.xml | 292 ----- app/src/main/res/layout-v21/feature_icon.xml | 49 - .../layout-v21/fragment_device_selection.xml | 49 - .../layout-v23/fragment_device_selection.xml | 61 - .../main/res/layout/activity_feature_bpm.xml | 286 ----- .../main/res/layout/activity_feature_cgms.xml | 218 ---- .../res/layout/activity_feature_cgms_item.xml | 54 - .../main/res/layout/activity_feature_csc.xml | 257 ---- .../main/res/layout/activity_feature_dfu.xml | 290 ----- .../main/res/layout/activity_feature_gls.xml | 219 ---- .../res/layout/activity_feature_gls_item.xml | 58 - .../layout/activity_feature_gls_subitem.xml | 48 - .../main/res/layout/activity_feature_hrs.xml | 147 --- .../main/res/layout/activity_feature_hts.xml | 112 -- .../res/layout/activity_feature_proximity.xml | 90 -- .../activity_feature_proximity_item.xml | 98 -- .../main/res/layout/activity_feature_rsc.xml | 292 ----- .../res/layout/activity_feature_template.xml | 159 --- .../main/res/layout/activity_feature_uart.xml | 60 - app/src/main/res/layout/activity_features.xml | 88 -- app/src/main/res/layout/activity_settings.xml | 40 - .../main/res/layout/activity_splashscreen.xml | 45 - app/src/main/res/layout/app_file_browser.xml | 43 - .../main/res/layout/app_file_browser_item.xml | 35 - app/src/main/res/layout/device_list_empty.xml | 27 - app/src/main/res/layout/device_list_row.xml | 55 - app/src/main/res/layout/device_list_title.xml | 26 - app/src/main/res/layout/drawer.xml | 72 -- app/src/main/res/layout/drawer_plugin.xml | 45 - .../res/layout/expandable_list_content.xml | 24 - app/src/main/res/layout/feature_icon.xml | 47 - .../main/res/layout/feature_uart_button.xml | 28 - .../res/layout/feature_uart_dialog_edit.xml | 100 -- .../layout/feature_uart_dialog_edit_icon.xml | 28 - .../feature_uart_dialog_new_configuration.xml | 61 - .../res/layout/feature_uart_dropdown_item.xml | 31 - .../layout/feature_uart_dropdown_title.xml | 41 - .../res/layout/fragment_device_selection.xml | 50 - .../layout/fragment_feature_uart_control.xml | 95 -- .../res/layout/fragment_feature_uart_log.xml | 69 -- app/src/main/res/layout/fragment_zip_info.xml | 37 - app/src/main/res/layout/log_item.xml | 40 - app/src/main/res/layout/toolbar.xml | 32 - app/src/main/res/layout/toolbar_w_spinner.xml | 39 - app/src/main/res/menu-port/uart_menu.xml | 37 - .../main/res/menu-port/uart_menu_config.xml | 36 - app/src/main/res/menu/gls_more.xml | 40 - app/src/main/res/menu/help.xml | 31 - app/src/main/res/menu/settings_and_about.xml | 37 - app/src/main/res/menu/uart_menu.xml | 31 - app/src/main/res/menu/uart_menu_config.xml | 31 - .../res/menu/uart_menu_configurations.xml | 55 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 26 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 26 +- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3129 -> 0 bytes app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 4450 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5152 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../main/res/mipmap-hdpi/ic_shortcut_dfu.png | Bin 2723 -> 0 bytes .../main/res/mipmap-hdpi/ic_shortcut_uart.png | Bin 2196 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2048 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 2675 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3182 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-mdpi/ic_shortcut_dfu.png | Bin 1552 -> 0 bytes .../main/res/mipmap-mdpi/ic_shortcut_uart.png | Bin 1236 -> 0 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4404 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 6341 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7362 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xhdpi/ic_shortcut_dfu.png | Bin 3264 -> 0 bytes .../res/mipmap-xhdpi/ic_shortcut_uart.png | Bin 2609 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 6974 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 10833 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 12039 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../res/mipmap-xxhdpi/ic_shortcut_dfu.png | Bin 5903 -> 0 bytes .../res/mipmap-xxhdpi/ic_shortcut_uart.png | Bin 4876 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10114 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 16696 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 17708 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../res/mipmap-xxxhdpi/ic_shortcut_dfu.png | Bin 8109 -> 0 bytes .../res/mipmap-xxxhdpi/ic_shortcut_uart.png | Bin 6623 -> 0 bytes app/src/main/res/values-land/dimens.xml | 29 - app/src/main/res/values-land/strings_bpm.xml | 26 - app/src/main/res/values-land/strings_cgms.xml | 27 - app/src/main/res/values-land/strings_csc.xml | 26 - app/src/main/res/values-land/strings_dfu.xml | 26 - app/src/main/res/values-land/strings_gls.xml | 26 - app/src/main/res/values-land/strings_hrs.xml | 26 - app/src/main/res/values-land/strings_hts.xml | 26 - .../res/values-land/strings_proximity.xml | 26 - app/src/main/res/values-land/strings_rsc.xml | 26 - app/src/main/res/values-night/themes.xml | 16 + app/src/main/res/values-sw600dp/dimens.xml | 42 - .../main/res/values-sw600dp/strings_bpm.xml | 26 - .../main/res/values-sw600dp/strings_csc.xml | 26 - .../main/res/values-sw600dp/strings_dfu.xml | 26 - .../main/res/values-sw600dp/strings_gls.xml | 26 - .../main/res/values-sw600dp/strings_hrs.xml | 26 - .../main/res/values-sw600dp/strings_hts.xml | 28 - .../main/res/values-sw600dp/strings_rsc.xml | 26 - .../main/res/values-sw720dp-land/dimens.xml | 36 - app/src/main/res/values-v19/styles.xml | 9 - app/src/main/res/values-v21/dimens.xml | 28 - app/src/main/res/values-v21/styles.xml | 62 - .../res/values-v21/styles_icon_button.xml | 47 - app/src/main/res/values-v23/styles.xml | 11 - app/src/main/res/values/attrs.xml | 28 - app/src/main/res/values/color.xml | 69 -- app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/dimens.xml | 45 - app/src/main/res/values/strings.xml | 76 +- app/src/main/res/values/strings_bpm.xml | 48 - app/src/main/res/values/strings_cgms.xml | 36 - app/src/main/res/values/strings_csc.xml | 175 --- app/src/main/res/values/strings_dfu.xml | 138 --- app/src/main/res/values/strings_gls.xml | 184 --- app/src/main/res/values/strings_hrs.xml | 47 - app/src/main/res/values/strings_hts.xml | 54 - app/src/main/res/values/strings_proximity.xml | 49 - app/src/main/res/values/strings_rsc.xml | 72 -- app/src/main/res/values/strings_template.xml | 53 - app/src/main/res/values/strings_uart.xml | 76 -- app/src/main/res/values/styles.xml | 147 --- .../main/res/values/styles_icon_button.xml | 47 - app/src/main/res/values/themes.xml | 25 + app/src/main/res/xml-v23/settings_dfu.xml | 58 - app/src/main/res/xml-v25/shortcuts.xml | 60 - app/src/main/res/xml/settings_csc.xml | 41 - app/src/main/res/xml/settings_dfu.xml | 58 - app/src/main/res/xml/settings_hts.xml | 34 - app/src/main/res/xml/settings_rsc.xml | 34 - app/src/main/res/xml/settings_template.xml | 34 - .../android/nrftoolbox/ExampleUnitTest.kt | 17 + build.gradle | 24 +- common/.gitignore | 2 - common/build.gradle | 29 - common/proguard-rules.pro | 17 - common/src/main/AndroidManifest.xml | 26 - .../nrftoolbox/utility/DebugLogger.java | 72 -- .../nrftoolbox/wearable/common/Constants.java | 67 -- common/src/main/res/values/strings.xml | 34 - gradle.properties | 15 +- gradle/wrapper/gradle-wrapper.jar | Bin 49896 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 +- gradlew | 149 ++- gradlew.bat | 179 ++- resources/scenario_1.png | Bin 45609 -> 0 bytes resources/scenario_2.png | Bin 49402 -> 0 bytes resources/structure.png | Bin 3399 -> 0 bytes settings.gradle | 25 +- wear/.gitignore | 2 - wear/build.gradle | 55 - wear/proguard-rules.pro | 17 - wear/src/main/AndroidManifest.xml | 83 -- .../android/nrftoolbox/DeviceItemLayout.java | 121 -- .../android/nrftoolbox/DevicesAdapter.java | 217 ---- .../android/nrftoolbox/ScannerActivity.java | 197 ---- .../android/nrftoolbox/ble/BleManager.java | 1044 ----------------- .../nrftoolbox/ble/BleManagerCallbacks.java | 113 -- .../android/nrftoolbox/ble/BleProfile.java | 184 --- .../android/nrftoolbox/ble/BleProfileApi.java | 443 ------- .../nrftoolbox/ble/BleProfileProvider.java | 36 - .../nrftoolbox/ble/BleProfileService.java | 388 ------ .../nrftoolbox/uart/UARTCommandsActivity.java | 277 ----- .../nrftoolbox/uart/UARTCommandsAdapter.java | 101 -- .../uart/UARTConfigurationItemLayout.java | 125 -- .../uart/UARTConfigurationsActivity.java | 236 ---- .../uart/UARTConfigurationsAdapter.java | 90 -- .../android/nrftoolbox/uart/UARTProfile.java | 150 --- .../nrftoolbox/uart/domain/Command.java | 161 --- .../uart/domain/UartConfiguration.java | 105 -- .../nrftoolbox/wearable/ActionReceiver.java | 78 -- .../wearable/MainWearableListenerService.java | 87 -- wear/src/main/res/color/item_background.xml | 28 - .../main/res/drawable-hdpi/ic_bluetooth.png | Bin 333 -> 0 bytes .../res/drawable-hdpi/ic_full_bluetooth.png | Bin 597 -> 0 bytes .../main/res/drawable-xhdpi/ic_bluetooth.png | Bin 340 -> 0 bytes .../res/drawable-xhdpi/ic_configurations.png | Bin 93 -> 0 bytes .../res/drawable-xhdpi/ic_full_bluetooth.png | Bin 1556 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_1.png | Bin 520 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_2.png | Bin 1259 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_3.png | Bin 1386 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_4.png | Bin 789 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_5.png | Bin 1115 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_6.png | Bin 1510 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_7.png | Bin 914 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_8.png | Bin 1738 -> 0 bytes .../src/main/res/drawable-xhdpi/ic_uart_9.png | Bin 1521 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_about.png | Bin 2971 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_down.png | Bin 586 -> 0 bytes .../res/drawable-xhdpi/ic_uart_forward.png | Bin 1063 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_left.png | Bin 564 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_pause.png | Bin 434 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_play.png | Bin 848 -> 0 bytes .../res/drawable-xhdpi/ic_uart_rewind.png | Bin 1073 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_right.png | Bin 536 -> 0 bytes .../res/drawable-xhdpi/ic_uart_settings.png | Bin 2452 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_stop.png | Bin 357 -> 0 bytes .../main/res/drawable-xhdpi/ic_uart_up.png | Bin 614 -> 0 bytes wear/src/main/res/drawable/ic_uart_action.xml | 45 - wear/src/main/res/layout/action_item.xml | 47 - .../main/res/layout/activity_grid_pager.xml | 41 - wear/src/main/res/layout/activity_list.xml | 42 - .../res/layout/activity_list_with_header.xml | 53 - .../main/res/layout/configuration_item.xml | 51 - wear/src/main/res/layout/device_item.xml | 62 - wear/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2419 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 4609 -> 0 bytes wear/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1588 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2800 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3328 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 6462 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 5249 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10436 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 7246 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 15052 -> 0 bytes wear/src/main/res/values/colors.xml | 32 - wear/src/main/res/values/strings.xml | 42 - 515 files changed, 986 insertions(+), 36459 deletions(-) delete mode 100644 app/libs/achartengine-1.2.0.jar delete mode 100644 app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ApplicationTest.java create mode 100644 app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/AppHelpFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/FeaturesActivity.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/SplashscreenActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ToolboxApplication.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/adapter/AppAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/app/ExpandableListActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecord.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecordsAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMSActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuInitiatorActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/NotificationActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/adapter/FileBrowserAppsAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/UploadCancelFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/ZipInfoFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/AboutDfuPreference.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/ExpandableRecordAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseRecord.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/LineGraphView.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/AlertLevelParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BloodPressureMeasurementParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BodySensorLocationParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMMeasurementParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMSpecificOpsControlPointParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CSCMeasurementParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/DateTimeParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementContextParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/HeartRateMeasurementParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/IntermediateCuffPressureParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RSCMeasurementParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RecordAccessControlPointParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureMeasurementParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureTypeParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemplateParser.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/LoggableBleManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileServiceReadyActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/IDeviceLogger.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/DeviceAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/LinkLossDialogFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTInterface.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLocalLogContentProvider.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogAdapter.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManagerCallbacks.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTNewConfigurationDialogFragment.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/ConfigurationContract.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/DatabaseHelper.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/NameColumns.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/UndoColumns.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/wearable/UARTConfigurationSynchronizer.java create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Color.kt create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Shape.kt create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Theme.kt create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Type.kt delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/FileHelper.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/ParserUtils.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/wearable/MainWearableListenerService.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ClosableSpinner.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/DividerItemDecoration.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundLinearLayout.java delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundRelativeLayout.java delete mode 100644 app/src/main/res/animator/click_animator.xml delete mode 100644 app/src/main/res/color/button_color.xml delete mode 100644 app/src/main/res/color/menu_text.xml delete mode 100644 app/src/main/res/drawable-hdpi/battery.png delete mode 100644 app/src/main/res/drawable-hdpi/drawer_shadow.9.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_bluetooth.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_action_disconnect.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_help.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_rssi_0_bar.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_rssi_1_bar.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_rssi_2_bars.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_rssi_3_bars.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_cgms.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_csc.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_hts.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_proximity.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_rsc.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_template.png delete mode 100644 app/src/main/res/drawable-hdpi/ic_stat_notify_uart.png delete mode 100644 app/src/main/res/drawable-hdpi/shadow_l.png delete mode 100644 app/src/main/res/drawable-hdpi/shadow_r.png delete mode 100644 app/src/main/res/drawable-v21/ic_feature_bg.xml delete mode 100644 app/src/main/res/drawable-v21/ic_icon_button_background.xml delete mode 100644 app/src/main/res/drawable-v21/uart_button_activated.xml delete mode 100644 app/src/main/res/drawable-v21/uart_button_background.xml delete mode 100644 app/src/main/res/drawable-v21/uart_button_normal.xml create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 app/src/main/res/drawable-xhdpi/app_drive.png delete mode 100644 app/src/main/res/drawable-xhdpi/app_file_manager.png delete mode 100644 app/src/main/res/drawable-xhdpi/app_google_play.png delete mode 100644 app/src/main/res/drawable-xhdpi/app_total_commander.png delete mode 100644 app/src/main/res/drawable-xhdpi/btn_default_focused_holo_light.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/drawer_shadow.9.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_bluetooth.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_action_disconnect.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_help.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_nrf_connect_feature_fg.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_cgms.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_csc.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_hts.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_proximity.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_rsc.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_template.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_stat_notify_uart.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_1.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_1_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_2.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_2_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_3.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_3_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_4.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_4_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_5.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_5_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_6.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_6_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_7.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_7_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_8.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_8_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_9.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_9_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_about.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_about_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_down.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_down_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_forward.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_forward_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_left.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_left_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_pause.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_pause_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_play.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_play_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_rewind.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_rewind_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_right.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_right_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_settings.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_settings_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_stop.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_stop_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_up.png delete mode 100644 app/src/main/res/drawable-xhdpi/ic_uart_up_small.png delete mode 100644 app/src/main/res/drawable-xhdpi/shadow_l.png delete mode 100644 app/src/main/res/drawable-xhdpi/shadow_r.png delete mode 100644 app/src/main/res/drawable-xhdpi/zip.png delete mode 100644 app/src/main/res/drawable-xxhdpi/action_bar_shadow.9.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_add_normal.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_add_pressed.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_bluetooth.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_clear_normal.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_clear_pressed.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_disconnect.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_download_normal.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_action_download_pressed.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_bluetooth.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_action_disconnect.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_battery_alert.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_battery_full.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_bpm_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_cgms_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_csc_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_dfu_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_glucose_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_hrs_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_hts_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_proximity_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_rsc_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_find.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_silent.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_template_feature.png delete mode 100644 app/src/main/res/drawable-xxxhdpi/ic_uart_feature.png delete mode 100644 app/src/main/res/drawable/app_file_browser.xml delete mode 100644 app/src/main/res/drawable/ic_action_add.xml delete mode 100644 app/src/main/res/drawable/ic_action_clear.xml delete mode 100644 app/src/main/res/drawable/ic_action_download.xml delete mode 100644 app/src/main/res/drawable/ic_battery.xml delete mode 100644 app/src/main/res/drawable/ic_feature_bg.xml delete mode 100644 app/src/main/res/drawable/ic_feature_bg_n.xml delete mode 100644 app/src/main/res/drawable/ic_feature_bg_p.xml delete mode 100644 app/src/main/res/drawable/ic_feature_small_bg.xml delete mode 100644 app/src/main/res/drawable/ic_feature_small_bg_n.xml delete mode 100644 app/src/main/res/drawable/ic_feature_small_bg_p.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml delete mode 100644 app/src/main/res/drawable/ic_nrf_connect_feature_small.xml delete mode 100644 app/src/main/res/drawable/ic_rssi_bar.xml delete mode 100644 app/src/main/res/drawable/nordic_logo.xml delete mode 100644 app/src/main/res/drawable/nordic_logo_horiz_white.xml delete mode 100644 app/src/main/res/drawable/start_edit_mode.xml delete mode 100644 app/src/main/res/drawable/stop_edit_mode.xml delete mode 100644 app/src/main/res/drawable/uart_button.xml delete mode 100644 app/src/main/res/drawable/uart_button_background.xml delete mode 100644 app/src/main/res/drawable/uart_button_small.xml delete mode 100644 app/src/main/res/drawable/uart_dialog_button_background.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_bpm.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_cgms.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_csc.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_dfu.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_gls.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_hrs.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_hts.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_proximity.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_rsc.xml delete mode 100644 app/src/main/res/layout-land/activity_feature_uart.xml delete mode 100644 app/src/main/res/layout-land/feature_uart_button.xml delete mode 100644 app/src/main/res/layout-land/fragment_feature_uart_control.xml delete mode 100644 app/src/main/res/layout-sw600dp-land/activity_feature_bpm.xml delete mode 100644 app/src/main/res/layout-sw600dp-land/activity_feature_csc.xml delete mode 100644 app/src/main/res/layout-sw600dp-land/activity_feature_dfu.xml delete mode 100644 app/src/main/res/layout-sw600dp-land/activity_feature_hrs.xml delete mode 100644 app/src/main/res/layout-sw600dp-land/feature_uart_button.xml delete mode 100644 app/src/main/res/layout-sw720dp-land/activity_feature_rsc.xml delete mode 100644 app/src/main/res/layout-v21/feature_icon.xml delete mode 100644 app/src/main/res/layout-v21/fragment_device_selection.xml delete mode 100644 app/src/main/res/layout-v23/fragment_device_selection.xml delete mode 100644 app/src/main/res/layout/activity_feature_bpm.xml delete mode 100644 app/src/main/res/layout/activity_feature_cgms.xml delete mode 100644 app/src/main/res/layout/activity_feature_cgms_item.xml delete mode 100644 app/src/main/res/layout/activity_feature_csc.xml delete mode 100644 app/src/main/res/layout/activity_feature_dfu.xml delete mode 100644 app/src/main/res/layout/activity_feature_gls.xml delete mode 100644 app/src/main/res/layout/activity_feature_gls_item.xml delete mode 100644 app/src/main/res/layout/activity_feature_gls_subitem.xml delete mode 100644 app/src/main/res/layout/activity_feature_hrs.xml delete mode 100644 app/src/main/res/layout/activity_feature_hts.xml delete mode 100644 app/src/main/res/layout/activity_feature_proximity.xml delete mode 100644 app/src/main/res/layout/activity_feature_proximity_item.xml delete mode 100644 app/src/main/res/layout/activity_feature_rsc.xml delete mode 100644 app/src/main/res/layout/activity_feature_template.xml delete mode 100644 app/src/main/res/layout/activity_feature_uart.xml delete mode 100644 app/src/main/res/layout/activity_features.xml delete mode 100644 app/src/main/res/layout/activity_settings.xml delete mode 100644 app/src/main/res/layout/activity_splashscreen.xml delete mode 100644 app/src/main/res/layout/app_file_browser.xml delete mode 100644 app/src/main/res/layout/app_file_browser_item.xml delete mode 100644 app/src/main/res/layout/device_list_empty.xml delete mode 100644 app/src/main/res/layout/device_list_row.xml delete mode 100644 app/src/main/res/layout/device_list_title.xml delete mode 100644 app/src/main/res/layout/drawer.xml delete mode 100644 app/src/main/res/layout/drawer_plugin.xml delete mode 100644 app/src/main/res/layout/expandable_list_content.xml delete mode 100644 app/src/main/res/layout/feature_icon.xml delete mode 100644 app/src/main/res/layout/feature_uart_button.xml delete mode 100644 app/src/main/res/layout/feature_uart_dialog_edit.xml delete mode 100644 app/src/main/res/layout/feature_uart_dialog_edit_icon.xml delete mode 100644 app/src/main/res/layout/feature_uart_dialog_new_configuration.xml delete mode 100644 app/src/main/res/layout/feature_uart_dropdown_item.xml delete mode 100644 app/src/main/res/layout/feature_uart_dropdown_title.xml delete mode 100644 app/src/main/res/layout/fragment_device_selection.xml delete mode 100644 app/src/main/res/layout/fragment_feature_uart_control.xml delete mode 100644 app/src/main/res/layout/fragment_feature_uart_log.xml delete mode 100644 app/src/main/res/layout/fragment_zip_info.xml delete mode 100644 app/src/main/res/layout/log_item.xml delete mode 100644 app/src/main/res/layout/toolbar.xml delete mode 100644 app/src/main/res/layout/toolbar_w_spinner.xml delete mode 100644 app/src/main/res/menu-port/uart_menu.xml delete mode 100644 app/src/main/res/menu-port/uart_menu_config.xml delete mode 100644 app/src/main/res/menu/gls_more.xml delete mode 100644 app/src/main/res/menu/help.xml delete mode 100644 app/src/main/res/menu/settings_and_about.xml delete mode 100644 app/src/main/res/menu/uart_menu.xml delete mode 100644 app/src/main/res/menu/uart_menu_config.xml delete mode 100644 app/src/main/res/menu/uart_menu_configurations.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-hdpi/ic_shortcut_dfu.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_shortcut_uart.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_shortcut_dfu.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_shortcut_uart.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_shortcut_dfu.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_shortcut_uart.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_shortcut_dfu.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_shortcut_uart.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_shortcut_dfu.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_shortcut_uart.png delete mode 100644 app/src/main/res/values-land/dimens.xml delete mode 100644 app/src/main/res/values-land/strings_bpm.xml delete mode 100644 app/src/main/res/values-land/strings_cgms.xml delete mode 100644 app/src/main/res/values-land/strings_csc.xml delete mode 100644 app/src/main/res/values-land/strings_dfu.xml delete mode 100644 app/src/main/res/values-land/strings_gls.xml delete mode 100644 app/src/main/res/values-land/strings_hrs.xml delete mode 100644 app/src/main/res/values-land/strings_hts.xml delete mode 100644 app/src/main/res/values-land/strings_proximity.xml delete mode 100644 app/src/main/res/values-land/strings_rsc.xml create mode 100644 app/src/main/res/values-night/themes.xml delete mode 100644 app/src/main/res/values-sw600dp/dimens.xml delete mode 100644 app/src/main/res/values-sw600dp/strings_bpm.xml delete mode 100644 app/src/main/res/values-sw600dp/strings_csc.xml delete mode 100644 app/src/main/res/values-sw600dp/strings_dfu.xml delete mode 100644 app/src/main/res/values-sw600dp/strings_gls.xml delete mode 100644 app/src/main/res/values-sw600dp/strings_hrs.xml delete mode 100644 app/src/main/res/values-sw600dp/strings_hts.xml delete mode 100644 app/src/main/res/values-sw600dp/strings_rsc.xml delete mode 100644 app/src/main/res/values-sw720dp-land/dimens.xml delete mode 100644 app/src/main/res/values-v19/styles.xml delete mode 100644 app/src/main/res/values-v21/dimens.xml delete mode 100644 app/src/main/res/values-v21/styles.xml delete mode 100644 app/src/main/res/values-v21/styles_icon_button.xml delete mode 100644 app/src/main/res/values-v23/styles.xml delete mode 100644 app/src/main/res/values/attrs.xml delete mode 100644 app/src/main/res/values/color.xml create mode 100644 app/src/main/res/values/colors.xml delete mode 100644 app/src/main/res/values/dimens.xml delete mode 100644 app/src/main/res/values/strings_bpm.xml delete mode 100644 app/src/main/res/values/strings_cgms.xml delete mode 100644 app/src/main/res/values/strings_csc.xml delete mode 100644 app/src/main/res/values/strings_dfu.xml delete mode 100644 app/src/main/res/values/strings_gls.xml delete mode 100644 app/src/main/res/values/strings_hrs.xml delete mode 100644 app/src/main/res/values/strings_hts.xml delete mode 100644 app/src/main/res/values/strings_proximity.xml delete mode 100644 app/src/main/res/values/strings_rsc.xml delete mode 100644 app/src/main/res/values/strings_template.xml delete mode 100644 app/src/main/res/values/strings_uart.xml delete mode 100644 app/src/main/res/values/styles.xml delete mode 100644 app/src/main/res/values/styles_icon_button.xml create mode 100644 app/src/main/res/values/themes.xml delete mode 100644 app/src/main/res/xml-v23/settings_dfu.xml delete mode 100644 app/src/main/res/xml-v25/shortcuts.xml delete mode 100644 app/src/main/res/xml/settings_csc.xml delete mode 100644 app/src/main/res/xml/settings_dfu.xml delete mode 100644 app/src/main/res/xml/settings_hts.xml delete mode 100644 app/src/main/res/xml/settings_rsc.xml delete mode 100644 app/src/main/res/xml/settings_template.xml create mode 100644 app/src/test/java/no/nordicsemi/android/nrftoolbox/ExampleUnitTest.kt delete mode 100644 common/.gitignore delete mode 100644 common/build.gradle delete mode 100644 common/proguard-rules.pro delete mode 100644 common/src/main/AndroidManifest.xml delete mode 100644 common/src/main/java/no/nordicsemi/android/nrftoolbox/utility/DebugLogger.java delete mode 100644 common/src/main/java/no/nordicsemi/android/nrftoolbox/wearable/common/Constants.java delete mode 100644 common/src/main/res/values/strings.xml mode change 100644 => 100755 gradlew delete mode 100644 resources/scenario_1.png delete mode 100644 resources/scenario_2.png delete mode 100644 resources/structure.png delete mode 100644 wear/.gitignore delete mode 100644 wear/build.gradle delete mode 100644 wear/proguard-rules.pro delete mode 100644 wear/src/main/AndroidManifest.xml delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/DeviceItemLayout.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/DevicesAdapter.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/ScannerActivity.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/ble/BleManager.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/ble/BleManagerCallbacks.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/ble/BleProfile.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/ble/BleProfileApi.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/ble/BleProfileProvider.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/ble/BleProfileService.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTCommandsActivity.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTCommandsAdapter.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationItemLayout.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsActivity.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTProfile.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/wearable/ActionReceiver.java delete mode 100644 wear/src/main/java/no/nordicsemi/android/nrftoolbox/wearable/MainWearableListenerService.java delete mode 100644 wear/src/main/res/color/item_background.xml delete mode 100644 wear/src/main/res/drawable-hdpi/ic_bluetooth.png delete mode 100644 wear/src/main/res/drawable-hdpi/ic_full_bluetooth.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_bluetooth.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_configurations.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_full_bluetooth.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_1.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_2.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_3.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_4.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_5.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_6.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_7.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_8.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_9.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_about.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_down.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_forward.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_left.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_pause.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_play.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_rewind.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_right.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_settings.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_stop.png delete mode 100644 wear/src/main/res/drawable-xhdpi/ic_uart_up.png delete mode 100644 wear/src/main/res/drawable/ic_uart_action.xml delete mode 100644 wear/src/main/res/layout/action_item.xml delete mode 100644 wear/src/main/res/layout/activity_grid_pager.xml delete mode 100644 wear/src/main/res/layout/activity_list.xml delete mode 100644 wear/src/main/res/layout/activity_list_with_header.xml delete mode 100644 wear/src/main/res/layout/configuration_item.xml delete mode 100644 wear/src/main/res/layout/device_item.xml delete mode 100644 wear/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 wear/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 wear/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 wear/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 wear/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 wear/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 wear/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 wear/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 wear/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 wear/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 wear/src/main/res/values/colors.xml delete mode 100644 wear/src/main/res/values/strings.xml diff --git a/.gitignore b/.gitignore index a8936c3b..01184e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,147 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/androidstudio +# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files .gradle -/local.properties -/.idea +.gradle/ +build/ + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +captures/ +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch +gen-external-apklibs + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA *.iml +*.iws +/out/ + +# User-specific configurations +.idea/ +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +# OS-specific files .DS_Store -/build -/key* \ No newline at end of file +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/androidstudio diff --git a/app/.gitignore b/app/.gitignore index e8fa30f8..6ae6c730 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,2 +1,123 @@ -/build + +# Created by https://www.toptal.com/developers/gitignore/api/androidstudio +# Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle +.gradle/ +build/ + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +captures/ +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch +gen-external-apklibs + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA *.iml +*.iws +/out/ + +# User-specific configurations +.idea/ + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) +hs_err_pid* + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/androidstudio diff --git a/app/build.gradle b/app/build.gradle index 04fcc59d..78f9dbe0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,76 +1,65 @@ -apply plugin: 'com.android.application' +plugins { + id 'com.android.application' + id 'kotlin-android' +} android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdk 31 defaultConfig { applicationId "no.nordicsemi.android.nrftoolbox" - minSdkVersion 18 - targetSdkVersion 29 - versionCode 78 - versionName "2.9.0" - resConfigs "en" + minSdk 21 + targetSdk 31 + versionCode 1 + versionName "1.0" - vectorDrawables.useSupportLibrary = true - } - buildTypes { - debug { - minifyEnabled false - shrinkResources false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true } + } + + buildTypes { release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { - targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + useIR = true + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + kotlinCompilerVersion '1.5.21' + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } } } dependencies { - implementation project(':common') - implementation fileTree(dir: 'libs', include: ['*.jar']) - wearApp project(path: ':wear') - // nRF Toolbox is using Play Service 10.2.0 in order to make the app working in China: - // https://developer.android.com/training/wearables/apps/creating-app-china.html - //noinspection GradleDependency - implementation 'com.google.android.gms:play-services-wearable:10.2.0' - - implementation 'androidx.appcompat:appcompat:1.3.0-alpha01' - implementation 'androidx.preference:preference:1.1.1' - implementation 'com.google.android.material:material:1.2.0-alpha06' - - implementation 'no.nordicsemi.android:log:2.2.0' - implementation 'no.nordicsemi.android.support.v18:scanner:1.4.3' - - // The DFU Library is imported automatically from jcenter: - implementation 'no.nordicsemi.android:dfu:1.10.3' - // if you desire to build the DFU Library, clone the - // https://github.com/NordicSemiconductor/Android-DFU-Library project into DFULibrary folder, - // add it as a module into the project structure and uncomment the following line - // (and also the according lines in the settings.gradle): - // implementation project(':dfu') - - // Gson is needed for DFU to work. The DFU library dependency to Gson is internal and would - // not be attached to APK. - // See: https://github.com/NordicSemiconductor/Android-nRF-Toolbox/issues/86 - - // Import the BLE Common Library. - // The BLE Common Library depends on BLE Library. It is enough to include the first one. - implementation 'no.nordicsemi.android:ble-common:2.2.0' - // The BLE Common Library may be included from jcenter. If you want to modify the code, - // clone both projects from GitHub and replace the line above with the following - // (and also the according lines in the settings.gradle): - // implementation project(':ble-common') - - implementation('org.simpleframework:simple-xml:2.7.1') { - exclude group: 'stax', module: 'stax-api' - exclude group: 'xpp3', module: 'xpp3' - } + implementation 'androidx.core:core-ktx:1.6.0' + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'com.google.android.material:material:1.4.0' + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.activity:activity-compose:1.3.1' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" } \ No newline at end of file diff --git a/app/libs/achartengine-1.2.0.jar b/app/libs/achartengine-1.2.0.jar deleted file mode 100644 index 21fe13d0da30876a51caf249404870886475360c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126046 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 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6f9aa2c1..481bb434 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,81 +1,21 @@ --optimizationpasses 5 --dontusemixedcaseclassnames --dontskipnonpubliclibraryclasses --dontskipnonpubliclibraryclassmembers --dontpreverify --dontshrink --verbose +# 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 --optimizations !code/simplification/arithmetic,!field/*,!class/merging/* +# 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 *; +#} --keepattributes *Annotation* --keepattributes Signature - --keepclasseswithmembernames class * { - native ; -} - --keepclasseswithmembers class * { - public (android.content.Context, android.util.AttributeSet); -} - --keep public class * extends android.app.Activity --keep public class * extends android.app.Application --keep public class * extends android.app.Service --keep public class * extends android.content.BroadcastReceiver --keep public class * extends android.content.ContentProvider --keep public class * extends android.app.backup.BackupAgentHelper --keep public class * extends android.preference.Preference --keep public class com.android.vending.licensing.ILicensingService - -# The AndroidX library contains references to newer platform versions. -# Don't warn about those in case this app is linking against an older platform version. --dontwarn androidx.** - --keep class com.google.android.gms.** --dontwarn com.google.android.gms.** - -# Java --keep class java.** { *; } --dontnote java.** --dontwarn java.** - --keep class javax.** { *; } --dontnote javax.** --dontwarn javax.** - --keep class sun.misc.Unsafe { *; } --dontnote sun.misc.Unsafe - --keep class javax.xml.stream.XMLOutputFactory { *; } - -# (the rt.jar has them) --dontwarn com.bea.xml.stream.XMLWriterBase --dontwarn javax.xml.stream.events.** --dontwarn javax.xml.stream.** - -# Simple XML --keep public class org.simpleframework.** { *; } --keep class org.simpleframework.xml.** { *; } --keep class org.simpleframework.xml.core.** { *; } --keep class org.simpleframework.xml.util.** { *; } - --keepattributes ElementList, Root, InnerClasses, LineNumberTable - --keepclasseswithmembers class * { - @org.simpleframework.xml.* ; -} - -# Chart Engine --keep class org.achartengine.** { *; } --dontnote org.achartengine.** - -# HTTP (might require legacyLibraries) ? --dontnote org.apache.http.params.** --dontnote org.apache.http.conn.scheme.** --dontnote org.apache.http.conn.** --dontnote android.net.http.** - -# DFU Library --keep class no.nordicsemi.android.dfu.** { *; } +# 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/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ApplicationTest.java b/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ApplicationTest.java deleted file mode 100644 index 5e6a0ca9..00000000 --- a/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ApplicationTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2015, 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; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt b/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..3b505247 --- /dev/null +++ b/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package no.nordicsemi.android.nrftoolbox + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("no.nordicsemi.android.nrftoolbox", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 74fba93b..0a69ceaf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,279 +1,25 @@ - - + - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppHelpFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppHelpFragment.java deleted file mode 100644 index f7fe26d7..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppHelpFragment.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2015, 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; - -import android.app.Dialog; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.appcompat.app.AlertDialog; - -public class AppHelpFragment extends DialogFragment { - private static final String ARG_TEXT = "ARG_TEXT"; - private static final String ARG_VERSION = "ARG_VERSION"; - - public static AppHelpFragment getInstance(final int aboutResId, final boolean appendVersion) { - final AppHelpFragment fragment = new AppHelpFragment(); - - final Bundle args = new Bundle(); - args.putInt(ARG_TEXT, aboutResId); - args.putBoolean(ARG_VERSION, appendVersion); - fragment.setArguments(args); - - return fragment; - } - - public static AppHelpFragment getInstance(final int aboutResId) { - final AppHelpFragment fragment = new AppHelpFragment(); - - final Bundle args = new Bundle(); - args.putInt(ARG_TEXT, aboutResId); - args.putBoolean(ARG_VERSION, false); - fragment.setArguments(args); - - return fragment; - } - - @Override - @NonNull - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final Bundle args = requireArguments(); - final StringBuilder text = new StringBuilder(getString(args.getInt(ARG_TEXT))); - - final boolean appendVersion = args.getBoolean(ARG_VERSION); - if (appendVersion) { - try { - final String version = requireContext().getPackageManager() - .getPackageInfo(requireContext().getPackageName(), 0).versionName; - text.append(getString(R.string.about_version, version)); - } catch (final NameNotFoundException e) { - // do nothing - } - } - return new AlertDialog.Builder(requireContext()) - .setTitle(R.string.about_title) - .setMessage(text) - .setPositiveButton(R.string.ok, null) - .create(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeaturesActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeaturesActivity.java deleted file mode 100644 index 88885987..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeaturesActivity.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (c) 2015, 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; - -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; -import android.net.Uri; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.core.view.GravityCompat; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.GridView; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.List; - -import no.nordicsemi.android.nrftoolbox.adapter.AppAdapter; -import no.nordicsemi.android.nrftoolbox.hr.HRActivity; - -public class FeaturesActivity extends AppCompatActivity { - private static final String NRF_CONNECT_CATEGORY = "no.nordicsemi.android.nrftoolbox.LAUNCHER"; - private static final String UTILS_CATEGORY = "no.nordicsemi.android.nrftoolbox.UTILS"; - private static final String NRF_CONNECT_PACKAGE = "no.nordicsemi.android.mcp"; - private static final String NRF_CONNECT_CLASS = NRF_CONNECT_PACKAGE + ".DeviceListActivity"; - private static final String NRF_CONNECT_MARKET_URI = "market://details?id=no.nordicsemi.android.mcp"; - - // Extras that can be passed from NFC (see SplashScreenActivity) - public static final String EXTRA_APP = "application/vnd.no.nordicsemi.type.app"; - public static final String EXTRA_ADDRESS = "application/vnd.no.nordicsemi.type.address"; - - private DrawerLayout drawerLayout; - private ActionBarDrawerToggle drawerToggle; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_features); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - - // ensure that Bluetooth exists - if (!ensureBLEExists()) - finish(); - - final DrawerLayout drawer = drawerLayout = findViewById(R.id.drawer_layout); - drawer.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); - - // Set the drawer toggle as the DrawerListener - drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close) { - @Override - public void onDrawerSlide(final View drawerView, final float slideOffset) { - // Disable the Hamburger icon animation - super.onDrawerSlide(drawerView, 0); - } - }; - drawer.addDrawerListener(drawerToggle); - - // setup plug-ins in the drawer - setupPluginsInDrawer(drawer.findViewById(R.id.plugin_container)); - - // configure the app grid - final GridView grid = findViewById(R.id.grid); - grid.setAdapter(new AppAdapter(this)); - grid.setEmptyView(findViewById(android.R.id.empty)); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - final Intent intent = getIntent(); - if (intent.hasExtra(EXTRA_APP) && intent.hasExtra(EXTRA_ADDRESS)) { - final String app = intent.getStringExtra(EXTRA_APP); - switch (app) { - case "HRM": - final Intent newIntent = new Intent(this, HRActivity.class); - newIntent.putExtra(EXTRA_ADDRESS, intent.getByteArrayExtra(EXTRA_ADDRESS)); - newIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - startActivity(newIntent); - break; - default: - // other are not supported yet - break; - } - } - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.help, menu); - return true; - } - - @Override - protected void onPostCreate(final Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - // Sync the toggle state after onRestoreInstanceState has occurred. - drawerToggle.syncState(); - } - - @Override - public void onConfigurationChanged(@NonNull final Configuration newConfig) { - super.onConfigurationChanged(newConfig); - drawerToggle.onConfigurationChanged(newConfig); - } - - @Override - public boolean onOptionsItemSelected(@NonNull final MenuItem item) { - // Pass the event to ActionBarDrawerToggle, if it returns - // true, then it has handled the app icon touch event - if (drawerToggle.onOptionsItemSelected(item)) { - return true; - } - - //noinspection SwitchStatementWithTooFewBranches - switch (item.getItemId()) { - case R.id.action_about: - final AppHelpFragment fragment = AppHelpFragment.getInstance(R.string.about_text, true); - fragment.show(getSupportFragmentManager(), null); - break; - } - return true; - } - - private void setupPluginsInDrawer(final ViewGroup container) { - final LayoutInflater inflater = LayoutInflater.from(this); - final PackageManager pm = getPackageManager(); - - // look for nRF Connect - final Intent nrfConnectIntent = new Intent(Intent.ACTION_MAIN); - nrfConnectIntent.addCategory(NRF_CONNECT_CATEGORY); - nrfConnectIntent.setClassName(NRF_CONNECT_PACKAGE, NRF_CONNECT_CLASS); - final ResolveInfo nrfConnectInfo = pm.resolveActivity(nrfConnectIntent, 0); - - // configure link to nRF Connect - final TextView nrfConnectItem = container.findViewById(R.id.link_mcp); - if (nrfConnectInfo == null) { - nrfConnectItem.setTextColor(Color.GRAY); - final ColorMatrix grayscale = new ColorMatrix(); - grayscale.setSaturation(0.0f); - nrfConnectItem.getCompoundDrawables()[0].mutate().setColorFilter(new ColorMatrixColorFilter(grayscale)); - } - nrfConnectItem.setOnClickListener(v -> { - Intent action = nrfConnectIntent; - if (nrfConnectInfo == null) - action = new Intent(Intent.ACTION_VIEW, Uri.parse(NRF_CONNECT_MARKET_URI)); - action.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - action.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - startActivity(action); - } catch (final ActivityNotFoundException e) { - Toast.makeText(FeaturesActivity.this, R.string.no_application_play, Toast.LENGTH_SHORT).show(); - } - drawerLayout.closeDrawers(); - }); - - // look for other plug-ins - final Intent utilsIntent = new Intent(Intent.ACTION_MAIN); - utilsIntent.addCategory(UTILS_CATEGORY); - - final List appList = pm.queryIntentActivities(utilsIntent, 0); - for (final ResolveInfo info : appList) { - final View item = inflater.inflate(R.layout.drawer_plugin, container, false); - final ImageView icon = item.findViewById(android.R.id.icon); - final TextView label = item.findViewById(android.R.id.text1); - - label.setText(info.loadLabel(pm)); - icon.setImageDrawable(info.loadIcon(pm)); - item.setOnClickListener(v -> { - final Intent intent = new Intent(); - intent.setComponent(new ComponentName(info.activityInfo.packageName, info.activityInfo.name)); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - drawerLayout.closeDrawers(); - }); - container.addView(item); - } - } - - private boolean ensureBLEExists() { - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { - Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show(); - return false; - } - return true; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt new file mode 100644 index 00000000..651ffcb9 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt @@ -0,0 +1,38 @@ +package no.nordicsemi.android.nrftoolbox + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.nrftoolbox.ui.theme.TestTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + TestTheme { + // A surface container using the 'background' color from the theme + Surface(color = MaterialTheme.colors.background) { + Greeting("Android") + } + } + } + } +} + +@Composable +fun Greeting(name: String) { + Text(text = "Hello $name!") +} + +@Preview(showBackground = true) +@Composable +fun DefaultPreview() { + TestTheme { + Greeting("Android") + } +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/SplashscreenActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/SplashscreenActivity.java deleted file mode 100644 index 5eaa1e9c..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/SplashscreenActivity.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2015, 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; - -import android.app.Activity; -import android.content.Intent; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.os.Bundle; -import android.os.Handler; -import android.os.Parcelable; - -public class SplashscreenActivity extends Activity { - /** Splash screen duration time in milliseconds */ - private static final int DELAY = 1000; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_splashscreen); - - // Jump to SensorsActivity after DELAY milliseconds - new Handler().postDelayed(() -> { - final Intent newIntent = new Intent(SplashscreenActivity.this, FeaturesActivity.class); - newIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - - // Handle NFC message, if app was opened using NFC AAR record - final Intent intent = getIntent(); - if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) { - final Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); - if (rawMsgs != null) { - for (Parcelable rawMsg : rawMsgs) { - final NdefMessage msg = (NdefMessage) rawMsg; - final NdefRecord[] records = msg.getRecords(); - - for (NdefRecord record : records) { - if (record.getTnf() == NdefRecord.TNF_MIME_MEDIA) { - switch (record.toMimeType()) { - case FeaturesActivity.EXTRA_APP: - newIntent.putExtra(FeaturesActivity.EXTRA_APP, new String(record.getPayload())); - break; - case FeaturesActivity.EXTRA_ADDRESS: - newIntent.putExtra(FeaturesActivity.EXTRA_ADDRESS, invertEndianness(record.getPayload())); - break; - } - } - } - } - } - } - startActivity(newIntent); - finish(); - }, DELAY); - } - - @Override - public void onBackPressed() { - // do nothing. Protect from exiting the application when splash screen is shown - } - - /** - * Inverts endianness of the byte array. - * @param bytes input byte array - * @return byte array in opposite order - */ - private byte[] invertEndianness(final byte[] bytes) { - if (bytes == null) - return null; - final int length = bytes.length; - final byte[] result = new byte[length]; - for (int i = 0; i < length; i++) - result[i] = bytes[length - i - 1]; - return result; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ToolboxApplication.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ToolboxApplication.java deleted file mode 100644 index 4726754a..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ToolboxApplication.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2017, 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; - -import android.app.Application; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.os.Build; - -import no.nordicsemi.android.dfu.DfuServiceInitiator; - -public class ToolboxApplication extends Application { - public static final String CONNECTED_DEVICE_CHANNEL = "connected_device_channel"; - public static final String FILE_SAVED_CHANNEL = "file_saved_channel"; - public static final String PROXIMITY_WARNINGS_CHANNEL = "proximity_warnings_channel"; - - @Override - public void onCreate() { - super.onCreate(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - DfuServiceInitiator.createDfuNotificationChannel(this); - - final NotificationChannel channel = new NotificationChannel(CONNECTED_DEVICE_CHANNEL, getString(R.string.channel_connected_devices_title), NotificationManager.IMPORTANCE_LOW); - channel.setDescription(getString(R.string.channel_connected_devices_description)); - channel.setShowBadge(false); - channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - - final NotificationChannel fileChannel = new NotificationChannel(FILE_SAVED_CHANNEL, getString(R.string.channel_files_title), NotificationManager.IMPORTANCE_LOW); - fileChannel.setDescription(getString(R.string.channel_files_description)); - fileChannel.setShowBadge(false); - fileChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); - - final NotificationChannel proximityChannel = new NotificationChannel(PROXIMITY_WARNINGS_CHANNEL, getString(R.string.channel_proximity_warnings_title), NotificationManager.IMPORTANCE_LOW); - proximityChannel.setDescription(getString(R.string.channel_proximity_warnings_description)); - proximityChannel.setShowBadge(false); - proximityChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); - - final NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.createNotificationChannel(channel); - notificationManager.createNotificationChannel(fileChannel); - notificationManager.createNotificationChannel(proximityChannel); - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/adapter/AppAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/adapter/AppAdapter.java deleted file mode 100644 index a61021a4..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/adapter/AppAdapter.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2015, 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.adapter; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import no.nordicsemi.android.nrftoolbox.R; - -public class AppAdapter extends BaseAdapter { - private static final String CATEGORY = "no.nordicsemi.android.nrftoolbox.LAUNCHER"; - private static final String NRF_CONNECT_PACKAGE = "no.nordicsemi.android.mcp"; - - private final Context context; - private final PackageManager packageManager; - private final LayoutInflater inflater; - private final List applications; - - public AppAdapter(@NonNull final Context context) { - this.context = context; - this.inflater = LayoutInflater.from(context); - - // get nRF installed app plugins from package manager - final PackageManager pm = packageManager = context.getPackageManager(); - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(CATEGORY); - - final List appList = applications = pm.queryIntentActivities(intent, 0); - // TODO remove the following loop after some time, when there will be no more MCP 1.1 at the market. - for (final ResolveInfo info : appList) { - if (NRF_CONNECT_PACKAGE.equals(info.activityInfo.packageName)) { - appList.remove(info); - break; - } - } - Collections.sort(appList, new ResolveInfo.DisplayNameComparator(pm)); - } - - @Override - public int getCount() { - return applications.size(); - } - - @Override - public Object getItem(final int position) { - return applications.get(position); - } - - @Override - public long getItemId(final int position) { - return position; - } - - @Override - public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) { - View view = convertView; - if (view == null) { - view = inflater.inflate(R.layout.feature_icon, parent, false); - - final ViewHolder holder = new ViewHolder(); - holder.view = view; - holder.icon = view.findViewById(R.id.icon); - holder.label = view.findViewById(R.id.label); - view.setTag(holder); - } - - final ResolveInfo info = applications.get(position); - final PackageManager pm = packageManager; - - final ViewHolder holder = (ViewHolder) view.getTag(); - holder.icon.setImageDrawable(info.loadIcon(pm)); - holder.label.setText(info.loadLabel(pm).toString().toUpperCase(Locale.US)); - holder.view.setOnClickListener(v -> { - final Intent intent = new Intent(); - intent.setComponent(new ComponentName(info.activityInfo.packageName, info.activityInfo.name)); - intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); - context.startActivity(intent); - }); - - return view; - } - - private class ViewHolder { - private View view; - private ImageView icon; - private TextView label; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/app/ExpandableListActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/app/ExpandableListActivity.java deleted file mode 100644 index ae7221aa..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/app/ExpandableListActivity.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package no.nordicsemi.android.nrftoolbox.app; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.database.Cursor; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View; -import android.view.View.OnCreateContextMenuListener; -import android.widget.ExpandableListAdapter; -import android.widget.ExpandableListView; -import android.widget.SimpleCursorTreeAdapter; -import android.widget.SimpleExpandableListAdapter; - -import java.util.List; -import java.util.Map; - -import no.nordicsemi.android.nrftoolbox.R; - -/** - * An activity that displays an expandable list of items by binding to a data source implementing the ExpandableListAdapter, and exposes event handlers when the - * user selects an item. - *

- * ExpandableListActivity hosts a {@link android.widget.ExpandableListView ExpandableListView} object that can be bound to different data sources that provide a - * two-levels of data (the top-level is group, and below each group are children). Binding, screen layout, and row layout are discussed in the following - * sections. - *

- * Screen Layout - *

- *

- * ExpandableListActivity has a default layout that consists of a single, full-screen, centered expandable list. However, if you desire, you can customize the - * screen layout by setting your own view layout with setContentView() in onCreate(). To do this, your own view MUST contain an ExpandableListView object with - * the id "@android:id/list" (or {@link android.R.id#list} if it's in code) - *

- * Optionally, your custom view can contain another view object of any type to display when the list view is empty. This "empty list" notifier must have an id - * "android:empty". Note that when an empty view is present, the expandable list view will be hidden when there is no data to display. - *

- * The following code demonstrates an (ugly) custom screen layout. It has a list with a green background, and an alternate red "no data" message. - *

- * - *
- * <?xml version="1.0" encoding="UTF-8"?>
- * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- *         android:orientation="vertical"
- *         android:layout_width="match_parent" 
- *         android:layout_height="match_parent"
- *         android:paddingLeft="8dp"
- *         android:paddingRight="8dp">
- * 
- *     <ExpandableListView android:id="@id/android:list"
- *               android:layout_width="match_parent" 
- *               android:layout_height="match_parent"
- *               android:background="#00FF00"
- *               android:layout_weight="1"
- *               android:drawSelectorOnTop="false"/>
- * 
- *     <TextView android:id="@id/android:empty"
- *               android:layout_width="match_parent" 
- *               android:layout_height="match_parent"
- *               android:background="#FF0000"
- *               android:text="No data"/>
- * </LinearLayout>
- * 
- * - *

- * Row Layout - *

- * The {@link ExpandableListAdapter} set in the {@link ExpandableListActivity} via {@link #setListAdapter(ExpandableListAdapter)} provides the {@link View}s for - * each row. This adapter has separate methods for providing the group {@link View}s and child {@link View}s. There are a couple provided - * {@link ExpandableListAdapter}s that simplify use of adapters: {@link SimpleCursorTreeAdapter} and {@link SimpleExpandableListAdapter}. - *

- * With these, you can specify the layout of individual rows for groups and children in the list. These constructor takes a few parameters that specify layout - * resources for groups and children. It also has additional parameters that let you specify which data field to associate with which object in the row layout - * resource. The {@link SimpleCursorTreeAdapter} fetches data from {@link Cursor}s and the {@link SimpleExpandableListAdapter} fetches data from {@link List}s - * of {@link Map}s. - *

- *

- * Android provides some standard row layout resources. These are in the {@link android.R.layout} class, and have names such as simple_list_item_1, - * simple_list_item_2, and two_line_list_item. The following layout XML is the source for the resource two_line_list_item, which displays two data fields,one - * above the other, for each list row. - *

- * - *
- * <?xml version="1.0" encoding="utf-8"?>
- * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- *     android:layout_width="match_parent"
- *     android:layout_height="wrap_content"
- *     android:orientation="vertical">
- * 
- *     <TextView android:id="@+id/text1"
- *         android:textSize="16sp"
- *         android:textStyle="bold"
- *         android:layout_width="match_parent"
- *         android:layout_height="wrap_content"/>
- * 
- *     <TextView android:id="@+id/text2"
- *         android:textSize="16sp"
- *         android:layout_width="match_parent"
- *         android:layout_height="wrap_content"/>
- * </LinearLayout>
- * 
- * - *

- * You must identify the data bound to each TextView object in this layout. The syntax for this is discussed in the next section. - *

- *

- * Binding to Data - *

- *

- * You bind the ExpandableListActivity's ExpandableListView object to data using a class that implements the {@link android.widget.ExpandableListAdapter - * ExpandableListAdapter} interface. Android provides two standard list adapters: {@link android.widget.SimpleExpandableListAdapter SimpleExpandableListAdapter} - * for static data (Maps), and {@link android.widget.SimpleCursorTreeAdapter SimpleCursorTreeAdapter} for Cursor query results. - *

- * - * @see #setListAdapter - * @see android.widget.ExpandableListView - */ -@SuppressLint("Registered") -@SuppressWarnings("unused") -public class ExpandableListActivity extends AppCompatActivity implements - OnCreateContextMenuListener, - ExpandableListView.OnChildClickListener, ExpandableListView.OnGroupCollapseListener, - ExpandableListView.OnGroupExpandListener { - ExpandableListAdapter adapter; - ExpandableListView list; - boolean finishedStart = false; - - /** - * Override this to populate the context menu when an item is long pressed. menuInfo will contain an - * {@link android.widget.ExpandableListView.ExpandableListContextMenuInfo} whose packedPosition is a packed position that should be used with - * {@link ExpandableListView#getPackedPositionType(long)} and the other similar methods. - *

- * {@inheritDoc} - */ - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - } - - /** - * Override this for receiving callbacks when a child has been clicked. - *

- * {@inheritDoc} - */ - @Override - public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, - int childPosition, long id) { - return false; - } - - /** - * Override this for receiving callbacks when a group has been collapsed. - */ - @Override - public void onGroupCollapse(int groupPosition) { - } - - /** - * Override this for receiving callbacks when a group has been expanded. - */ - @Override - public void onGroupExpand(int groupPosition) { - } - - /** - * Ensures the expandable list view has been created before Activity restores all of the view states. - * - * @see Activity#onRestoreInstanceState(Bundle) - */ - @Override - protected void onRestoreInstanceState(@NonNull Bundle state) { - ensureList(); - super.onRestoreInstanceState(state); - } - - /** - * Updates the screen state (current list and other views) when the content changes. - * - * @see androidx.appcompat.app.AppCompatActivity#onContentChanged() - */ - @Override - public void onContentChanged() { - super.onContentChanged(); - final View emptyView = findViewById(R.id.empty); - list = findViewById(R.id.list); - if (list == null) { - throw new RuntimeException( - "Your content must have a ExpandableListView whose id attribute is " + - "'R.id.list'"); - } - if (emptyView != null) { - list.setEmptyView(emptyView); - } - list.setOnChildClickListener(this); - list.setOnGroupExpandListener(this); - list.setOnGroupCollapseListener(this); - - if (finishedStart) { - setListAdapter(adapter); - } - finishedStart = true; - } - - /** - * Provide the adapter for the expandable list. - */ - public void setListAdapter(final ExpandableListAdapter adapter) { - synchronized (this) { - ensureList(); - this.adapter = adapter; - list.setAdapter(adapter); - } - } - - /** - * Get the activity's expandable list view widget. This can be used to get the selection, set the selection, and many other useful functions. - * - * @see ExpandableListView - */ - public ExpandableListView getExpandableListView() { - ensureList(); - return list; - } - - /** - * Get the ExpandableListAdapter associated with this activity's ExpandableListView. - */ - public ExpandableListAdapter getExpandableListAdapter() { - return adapter; - } - - private void ensureList() { - if (list != null) { - return; - } - setContentView(R.layout.expandable_list_content); - } - - /** - * Gets the ID of the currently selected group or child. - * - * @return The ID of the currently selected group or child. - */ - public long getSelectedId() { - return list.getSelectedId(); - } - - /** - * Gets the position (in packed position representation) of the currently selected group or child. Use {@link ExpandableListView#getPackedPositionType}, - * {@link ExpandableListView#getPackedPositionGroup}, and {@link ExpandableListView#getPackedPositionChild} to unpack the returned packed position. - * - * @return A packed position representation containing the currently selected group or child's position and type. - */ - public long getSelectedPosition() { - return list.getSelectedPosition(); - } - - /** - * Sets the selection to the specified child. If the child is in a collapsed group, the group will only be expanded and child subsequently selected if - * shouldExpandGroup is set to true, otherwise the method will return false. - * - * @param groupPosition - * The position of the group that contains the child. - * @param childPosition - * The position of the child within the group. - * @param shouldExpandGroup - * Whether the child's group should be expanded if it is collapsed. - * @return Whether the selection was successfully set on the child. - */ - public boolean setSelectedChild(final int groupPosition, final int childPosition, final boolean shouldExpandGroup) { - return list.setSelectedChild(groupPosition, childPosition, shouldExpandGroup); - } - - /** - * Sets the selection to the specified group. - * - * @param groupPosition - * The position of the group that should be selected. - */ - public void setSelectedGroup(final int groupPosition) { - list.setSelectedGroup(groupPosition); - } - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManager.java deleted file mode 100644 index 36cf52d5..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManager.java +++ /dev/null @@ -1,126 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.battery; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; - -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import android.util.Log; - -import java.util.UUID; - -import no.nordicsemi.android.ble.BleManager; -import no.nordicsemi.android.ble.callback.DataReceivedCallback; -import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -/** - * The Ble Manager with Battery Service support. - * - * @param The profile callbacks type. - * @see BleManager - */ -public abstract class BatteryManager extends LoggableBleManager { - /** Battery Service UUID. */ - private final static UUID BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb"); - /** Battery Level characteristic UUID. */ - private final static UUID BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb"); - - private BluetoothGattCharacteristic batteryLevelCharacteristic; - /** Last received Battery Level value. */ - private Integer batteryLevel; - - /** - * The manager constructor. - * - * @param context context. - */ - public BatteryManager(final Context context) { - super(context); - } - - private DataReceivedCallback batteryLevelDataCallback = new BatteryLevelDataCallback() { - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, - @IntRange(from = 0, to = 100) final int batteryLevel) { - log(LogContract.Log.Level.APPLICATION,"Battery Level received: " + batteryLevel + "%"); - BatteryManager.this.batteryLevel = batteryLevel; - mCallbacks.onBatteryLevelChanged(device, batteryLevel); - } - - @Override - public void onInvalidDataReceived(@NonNull final BluetoothDevice device, final @NonNull Data data) { - log(Log.WARN, "Invalid Battery Level data received: " + data); - } - }; - - public void readBatteryLevelCharacteristic() { - if (isConnected()) { - readCharacteristic(batteryLevelCharacteristic) - .with(batteryLevelDataCallback) - .fail((device, status) -> log(Log.WARN,"Battery Level characteristic not found")) - .enqueue(); - } - } - - public void enableBatteryLevelCharacteristicNotifications() { - if (isConnected()) { - // If the Battery Level characteristic is null, the request will be ignored - setNotificationCallback(batteryLevelCharacteristic) - .with(batteryLevelDataCallback); - enableNotifications(batteryLevelCharacteristic) - .done(device -> log(Log.INFO, "Battery Level notifications enabled")) - .fail((device, status) -> log(Log.WARN, "Battery Level characteristic not found")) - .enqueue(); - } - } - - /** - * Disables Battery Level notifications on the Server. - */ - public void disableBatteryLevelCharacteristicNotifications() { - if (isConnected()) { - disableNotifications(batteryLevelCharacteristic) - .done(device -> log(Log.INFO, "Battery Level notifications disabled")) - .enqueue(); - } - } - - /** - * Returns the last received Battery Level value. - * The value is set to null when the device disconnects. - * @return Battery Level value, in percent. - */ - public Integer getBatteryLevel() { - return batteryLevel; - } - - protected abstract class BatteryManagerGattCallback extends BleManagerGattCallback { - - @Override - protected void initialize() { - readBatteryLevelCharacteristic(); - enableBatteryLevelCharacteristicNotifications(); - } - - @Override - protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(BATTERY_SERVICE_UUID); - if (service != null) { - batteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID); - } - return batteryLevelCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - batteryLevelCharacteristic = null; - batteryLevel = null; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManagerCallbacks.java deleted file mode 100644 index 26a2905b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/battery/BatteryManagerCallbacks.java +++ /dev/null @@ -1,7 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.battery; - -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback; - -public interface BatteryManagerCallbacks extends BleManagerCallbacks, BatteryLevelCallback { -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMActivity.java deleted file mode 100644 index 8290c12e..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMActivity.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2015, 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.bpm; - -import android.bluetooth.BluetoothDevice; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.widget.TextView; - -import java.util.Calendar; -import java.util.UUID; - -import no.nordicsemi.android.ble.common.profile.bp.BloodPressureMeasurementCallback; -import no.nordicsemi.android.ble.common.profile.bp.IntermediateCuffPressureCallback; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileActivity; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -// TODO The BPMActivity should be rewritten to use the service approach, like other do. -public class BPMActivity extends BleProfileActivity implements BPMManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "BPMActivity"; - - private TextView systolicView; - private TextView systolicUnitView; - private TextView diastolicView; - private TextView diastolicUnitView; - private TextView meanAPView; - private TextView meanAPUnitView; - private TextView pulseView; - private TextView timestampView; - private TextView batteryLevelView; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_bpm); - setGUI(); - } - - private void setGUI() { - systolicView = findViewById(R.id.systolic); - systolicUnitView = findViewById(R.id.systolic_unit); - diastolicView = findViewById(R.id.diastolic); - diastolicUnitView = findViewById(R.id.diastolic_unit); - meanAPView = findViewById(R.id.mean_ap); - meanAPUnitView = findViewById(R.id.mean_ap_unit); - pulseView = findViewById(R.id.pulse); - timestampView = findViewById(R.id.timestamp); - batteryLevelView = findViewById(R.id.battery); - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.bpm_feature_title; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.bpm_default_name; - } - - @Override - protected int getAboutTextId() { - return R.string.bpm_about_text; - } - - @Override - protected UUID getFilterUUID() { - return BPMManager.BP_SERVICE_UUID; - } - - @Override - protected LoggableBleManager initializeManager() { - final BPMManager manager = BPMManager.getBPMManager(getApplicationContext()); - manager.setGattCallbacks(this); - return manager; - } - - @Override - protected void setDefaultUI() { - systolicView.setText(R.string.not_available_value); - systolicUnitView.setText(null); - diastolicView.setText(R.string.not_available_value); - diastolicUnitView.setText(null); - meanAPView.setText(R.string.not_available_value); - meanAPUnitView.setText(null); - pulseView.setText(R.string.not_available_value); - timestampView.setText(R.string.not_available); - batteryLevelView.setText(R.string.not_available); - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // this may notify user or show some views - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - // this may notify user - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - runOnUiThread(() -> batteryLevelView.setText(R.string.not_available)); - } - - @Override - public void onBloodPressureMeasurementReceived(@NonNull final BluetoothDevice device, - final float systolic, final float diastolic, final float meanArterialPressure, final int unit, - @Nullable final Float pulseRate, @Nullable final Integer userID, - @Nullable final BPMStatus status, @Nullable final Calendar calendar) { - runOnUiThread(() -> { - systolicView.setText(String.valueOf(systolic)); - diastolicView.setText(String.valueOf(diastolic)); - meanAPView.setText(String.valueOf(meanArterialPressure)); - if (pulseRate != null) - pulseView.setText(String.valueOf(pulseRate)); - else - pulseView.setText(R.string.not_available_value); - if (calendar != null) - timestampView.setText(getString(R.string.bpm_timestamp, calendar)); - else - timestampView.setText(R.string.not_available); - - systolicUnitView.setText(unit == BloodPressureMeasurementCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa); - diastolicUnitView.setText(unit == BloodPressureMeasurementCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa); - meanAPUnitView.setText(unit == BloodPressureMeasurementCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa); - }); - } - - @Override - public void onIntermediateCuffPressureReceived(@NonNull final BluetoothDevice device, final float cuffPressure, final int unit, - @Nullable final Float pulseRate, @Nullable final Integer userID, - @Nullable final BPMStatus status, @Nullable final Calendar calendar) { - runOnUiThread(() -> { - systolicView.setText(String.valueOf(cuffPressure)); - diastolicView.setText(R.string.not_available_value); - meanAPView.setText(R.string.not_available_value); - if (pulseRate != null) - pulseView.setText(String.valueOf(pulseRate)); - else - pulseView.setText(R.string.not_available_value); - if (calendar != null) - timestampView.setText(getString(R.string.bpm_timestamp, calendar)); - else - timestampView.setText(R.string.not_available); - - systolicUnitView.setText(unit == IntermediateCuffPressureCallback.UNIT_mmHg ? R.string.bpm_unit_mmhg : R.string.bpm_unit_kpa); - diastolicUnitView.setText(null); - meanAPUnitView.setText(null); - }); - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - runOnUiThread(() -> batteryLevelView.setText(getString(R.string.battery, batteryLevel))); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManager.java deleted file mode 100644 index d452f9c7..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManager.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (c) 2015, 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.bpm; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.util.Log; - -import java.util.Calendar; -import java.util.UUID; - -import no.nordicsemi.android.ble.common.callback.bps.BloodPressureMeasurementDataCallback; -import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureDataCallback; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.BloodPressureMeasurementParser; -import no.nordicsemi.android.nrftoolbox.parser.IntermediateCuffPressureParser; - -@SuppressWarnings({"unused", "WeakerAccess"}) -public class BPMManager extends BatteryManager { - /** Blood Pressure service UUID. */ - public final static UUID BP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb"); - /** Blood Pressure Measurement characteristic UUID. */ - private static final UUID BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb"); - /** Intermediate Cuff Pressure characteristic UUID. */ - private static final UUID ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb"); - - private BluetoothGattCharacteristic bpmCharacteristic, icpCharacteristic; - - private static BPMManager managerInstance = null; - - /** - * Returns the singleton implementation of BPMManager. - */ - public static synchronized BPMManager getBPMManager(final Context context) { - if (managerInstance == null) { - managerInstance = new BPMManager(context); - } - return managerInstance; - } - - private BPMManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new BloodPressureManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving notification, etc. - */ - private class BloodPressureManagerGattCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - super.initialize(); - - setNotificationCallback(icpCharacteristic) - .with(new IntermediateCuffPressureDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + IntermediateCuffPressureParser.parse(data) + "\" received"); - - // Pass through received data - super.onDataReceived(device, data); - } - - @Override - public void onIntermediateCuffPressureReceived(@NonNull final BluetoothDevice device, - final float cuffPressure, final int unit, - @Nullable final Float pulseRate, @Nullable final Integer userID, - @Nullable final BPMStatus status, @Nullable final Calendar calendar) { - mCallbacks.onIntermediateCuffPressureReceived(device, cuffPressure, unit, pulseRate, userID, status, calendar); - } - - @Override - public void onInvalidDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(Log.WARN, "Invalid ICP data received: " + data); - } - }); - setIndicationCallback(bpmCharacteristic) - .with(new BloodPressureMeasurementDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + BloodPressureMeasurementParser.parse(data) + "\" received"); - - // Pass through received data - super.onDataReceived(device, data); - } - - @Override - public void onBloodPressureMeasurementReceived(@NonNull final BluetoothDevice device, - final float systolic, final float diastolic, final float meanArterialPressure, - final int unit, @Nullable final Float pulseRate, - @Nullable final Integer userID, @Nullable final BPMStatus status, - @Nullable final Calendar calendar) { - mCallbacks.onBloodPressureMeasurementReceived(device, systolic, diastolic, - meanArterialPressure, unit, pulseRate, userID, status, calendar); - } - - @Override - public void onInvalidDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(Log.WARN, "Invalid BPM data received: " + data); - } - }); - - enableNotifications(icpCharacteristic) - .fail((device, status) -> log(Log.WARN, - "Intermediate Cuff Pressure characteristic not found")) - .enqueue(); - enableIndications(bpmCharacteristic).enqueue(); - } - - @Override - protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(BP_SERVICE_UUID); - if (service != null) { - bpmCharacteristic = service.getCharacteristic(BPM_CHARACTERISTIC_UUID); - icpCharacteristic = service.getCharacteristic(ICP_CHARACTERISTIC_UUID); - } - return bpmCharacteristic != null; - } - - @Override - protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) { - super.isOptionalServiceSupported(gatt); // ignore the result of this - return icpCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - icpCharacteristic = null; - bpmCharacteristic = null; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManagerCallbacks.java deleted file mode 100644 index 75f550b8..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/bpm/BPMManagerCallbacks.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2015, 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.bpm; - -import no.nordicsemi.android.ble.common.profile.bp.BloodPressureMeasurementCallback; -import no.nordicsemi.android.ble.common.profile.bp.IntermediateCuffPressureCallback; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -interface BPMManagerCallbacks extends BatteryManagerCallbacks, - BloodPressureMeasurementCallback, IntermediateCuffPressureCallback { - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManager.java deleted file mode 100644 index cd8bba53..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManager.java +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright (c) 2016, 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.cgm; - -import android.annotation.SuppressLint; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import android.util.Log; -import android.util.SparseArray; - -import java.util.UUID; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback; -import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureDataCallback; -import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointDataCallback; -import no.nordicsemi.android.ble.common.callback.cgm.CGMStatusDataCallback; -import no.nordicsemi.android.ble.common.callback.cgm.ContinuousGlucoseMeasurementDataCallback; -import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData; -import no.nordicsemi.android.ble.common.data.cgm.CGMSpecificOpsControlPointData; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.CGMMeasurementParser; -import no.nordicsemi.android.nrftoolbox.parser.CGMSpecificOpsControlPointParser; -import no.nordicsemi.android.nrftoolbox.parser.RecordAccessControlPointParser; - -class CGMManager extends BatteryManager { - /** Cycling Speed and Cadence service UUID. */ - static final UUID CGMS_UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb"); - private static final UUID CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb"); - private static final UUID CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb"); - private static final UUID CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb"); - private static final UUID CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb"); - /** Record Access Control Point characteristic UUID. */ - private static final UUID RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb"); - - private BluetoothGattCharacteristic cgmStatusCharacteristic; - private BluetoothGattCharacteristic cgmFeatureCharacteristic; - private BluetoothGattCharacteristic cgmMeasurementCharacteristic; - private BluetoothGattCharacteristic cgmSpecificOpsControlPointCharacteristic; - private BluetoothGattCharacteristic recordAccessControlPointCharacteristic; - - private SparseArray records = new SparseArray<>(); - - /** A flag set to true if the remote device supports E2E CRC. */ - private boolean secured; - /** - * A flag set when records has been requested using RACP. This is to distinguish CGM packets - * received as continuous measurements or requested. - */ - private boolean recordAccessRequestInProgress; - /** - * The timestamp when the session has started. This is needed to display the user facing - * times of samples. - */ - private long sessionStartTime; - - CGMManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new CGMManagerGattCallback(); - } - - /** - * BluetoothGatt mCallbacks for connection/disconnection, service discovery, - * receiving notification, etc. - */ - private class CGMManagerGattCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - // Enable Battery service - super.initialize(); - - // Read CGM Feature characteristic, mainly to see if the device supports E2E CRC. - // This is not supported in the experimental CGMS from the SDK. - readCharacteristic(cgmFeatureCharacteristic) - .with(new CGMFeatureDataCallback() { - @Override - public void onContinuousGlucoseMonitorFeaturesReceived(@NonNull final BluetoothDevice device, @NonNull final CGMFeatures features, - final int type, final int sampleLocation, final boolean secured) { - CGMManager.this.secured = features.e2eCrcSupported; - log(LogContract.Log.Level.APPLICATION, "E2E CRC feature " + (CGMManager.this.secured ? "supported" : "not supported")); - } - }) - .fail((device, status) -> log(Log.WARN, "Could not read CGM Feature characteristic")) - .enqueue(); - - // Check if the session is already started. This is not supported in the experimental CGMS from the SDK. - readCharacteristic(cgmStatusCharacteristic) - .with(new CGMStatusDataCallback() { - @Override - public void onContinuousGlucoseMonitorStatusChanged(@NonNull final BluetoothDevice device, @NonNull final CGMStatus status, final int timeOffset, final boolean secured) { - if (!status.sessionStopped) { - sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L; - log(LogContract.Log.Level.APPLICATION, "Session already started"); - } - } - }) - .fail((device, status) -> log(Log.WARN, "Could not read CGM Status characteristic")) - .enqueue(); - - // Set notification and indication mCallbacks - setNotificationCallback(cgmMeasurementCharacteristic) - .with(new ContinuousGlucoseMeasurementDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + CGMMeasurementParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @Override - public void onContinuousGlucoseMeasurementReceived(@NonNull final BluetoothDevice device, - final float glucoseConcentration, - @Nullable final Float cgmTrend, - @Nullable final Float cgmQuality, - final CGMStatus status, - final int timeOffset, - final boolean secured) { - // If the CGM Status characteristic has not been read and the session was already started before, - // estimate the Session Start Time by subtracting timeOffset minutes from the current timestamp. - if (sessionStartTime == 0 && !recordAccessRequestInProgress) { - sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L; - } - - // Calculate the sample timestamp based on the Session Start Time - final long timestamp = sessionStartTime + (timeOffset * 60000L); // Sequence number is in minutes since Start Session - - final CGMRecord record = new CGMRecord(timeOffset, glucoseConcentration, timestamp); - records.put(record.sequenceNumber, record); - mCallbacks.onCGMValueReceived(device, record); - } - - @Override - public void onContinuousGlucoseMeasurementReceivedWithCrcError(@NonNull final BluetoothDevice device, - @NonNull final Data data) { - log(Log.WARN, "Continuous Glucose Measurement record received with CRC error"); - } - }); - - setIndicationCallback(cgmSpecificOpsControlPointCharacteristic) - .with(new CGMSpecificOpsControlPointDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + CGMSpecificOpsControlPointParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @SuppressLint("SwitchIntDef") - @Override - public void onCGMSpecificOpsOperationCompleted(@NonNull final BluetoothDevice device, - @CGMOpCode final int requestCode, - final boolean secured) { - switch (requestCode) { - case CGM_OP_CODE_START_SESSION: - sessionStartTime = System.currentTimeMillis(); - break; - case CGM_OP_CODE_STOP_SESSION: - sessionStartTime = 0; - break; - } - } - - @SuppressLint("SwitchIntDef") - @SuppressWarnings("StatementWithEmptyBody") - @Override - public void onCGMSpecificOpsOperationError(@NonNull final BluetoothDevice device, - @CGMOpCode final int requestCode, - @CGMErrorCode final int errorCode, - final boolean secured) { - switch (requestCode) { - case CGM_OP_CODE_START_SESSION: - if (errorCode == CGM_ERROR_PROCEDURE_NOT_COMPLETED) { - // Session was already started before. - // Looks like the CGM Status characteristic has not been read, - // otherwise we would have got the Session Start Time before. - // The Session Start Time will be calculated when a next CGM - // packet is received based on it's Time Offset. - } - case CGM_OP_CODE_STOP_SESSION: - sessionStartTime = 0; - break; - } - } - - @Override - public void onCGMSpecificOpsResponseReceivedWithCrcError(@NonNull final BluetoothDevice device, - @NonNull final Data data) { - log(Log.ERROR, "Request failed: CRC error"); - } - }); - - setIndicationCallback(recordAccessControlPointCharacteristic) - .with(new RecordAccessControlPointDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @SuppressLint("SwitchIntDef") - @Override - public void onRecordAccessOperationCompleted(@NonNull final BluetoothDevice device, - @RACPOpCode final int requestCode) { - //noinspection SwitchStatementWithTooFewBranches - switch (requestCode) { - case RACP_OP_CODE_ABORT_OPERATION: - mCallbacks.onOperationAborted(device); - break; - default: - recordAccessRequestInProgress = false; - mCallbacks.onOperationCompleted(device); - break; - } - } - - @Override - public void onRecordAccessOperationCompletedWithNoRecordsFound(@NonNull final BluetoothDevice device, - @RACPOpCode final int requestCode) { - recordAccessRequestInProgress = false; - mCallbacks.onOperationCompleted(device); - } - - @Override - public void onNumberOfRecordsReceived(@NonNull final BluetoothDevice device, final int numberOfRecords) { - mCallbacks.onNumberOfRecordsRequested(device, numberOfRecords); - if (numberOfRecords > 0) { - if (records.size() > 0) { - final int sequenceNumber = records.keyAt(records.size() - 1) + 1; - writeCharacteristic(recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)) - .enqueue(); - } else { - writeCharacteristic(recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportAllStoredRecords()) - .enqueue(); - } - } else { - recordAccessRequestInProgress = false; - mCallbacks.onOperationCompleted(device); - } - } - - @Override - public void onRecordAccessOperationError(@NonNull final BluetoothDevice device, - @RACPOpCode final int requestCode, - @RACPErrorCode final int errorCode) { - log(Log.WARN, "Record Access operation failed (error " + errorCode + ")"); - if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - mCallbacks.onOperationNotSupported(device); - } else { - mCallbacks.onOperationFailed(device); - } - } - }); - - // Enable notifications and indications - enableNotifications(cgmMeasurementCharacteristic) - .fail((device, status) -> log(Log.WARN, "Failed to enable Continuous Glucose Measurement notifications (" + status + ")")) - .enqueue(); - enableIndications(cgmSpecificOpsControlPointCharacteristic) - .fail((device, status) -> log(Log.WARN, "Failed to enable CGM Specific Ops Control Point indications notifications (" + status + ")")) - .enqueue(); - enableIndications(recordAccessControlPointCharacteristic) - .fail((device, status) -> log(Log.WARN, "Failed to enabled Record Access Control Point indications (error " + status + ")")) - .enqueue(); - - // Start Continuous Glucose session if hasn't been started before - if (sessionStartTime == 0L) { - writeCharacteristic(cgmSpecificOpsControlPointCharacteristic, CGMSpecificOpsControlPointData.startSession(secured)) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + CGMSpecificOpsControlPointParser.parse(data) + "\" sent")) - .fail((device, status) -> log(LogContract.Log.Level.ERROR, "Failed to start session (error " + status + ")")) - .enqueue(); - } - } - - @Override - protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(CGMS_UUID); - if (service != null) { - cgmStatusCharacteristic = service.getCharacteristic(CGM_STATUS_UUID); - cgmFeatureCharacteristic = service.getCharacteristic(CGM_FEATURE_UUID); - cgmMeasurementCharacteristic = service.getCharacteristic(CGM_MEASUREMENT_UUID); - cgmSpecificOpsControlPointCharacteristic = service.getCharacteristic(CGM_OPS_CONTROL_POINT_UUID); - recordAccessControlPointCharacteristic = service.getCharacteristic(RACP_UUID); - } - return cgmMeasurementCharacteristic != null - && cgmSpecificOpsControlPointCharacteristic != null - && recordAccessControlPointCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - super.onDeviceDisconnected(); - cgmStatusCharacteristic = null; - cgmFeatureCharacteristic = null; - cgmMeasurementCharacteristic = null; - cgmSpecificOpsControlPointCharacteristic = null; - recordAccessControlPointCharacteristic = null; - } - } - - /** - * Returns a list of CGM records obtained from this device. The key in the array is the - */ - SparseArray getRecords() { - return records; - } - - /** - * Clears the records list locally - */ - void clear() { - records.clear(); - mCallbacks.onDataSetCleared(getBluetoothDevice()); - } - - /** - * Sends the request to obtain the last (most recent) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ - void getLastRecord() { - if (recordAccessControlPointCharacteristic == null) - return; - - clear(); - mCallbacks.onOperationStarted(getBluetoothDevice()); - recordAccessRequestInProgress = true; - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportLastStoredRecord()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends the request to obtain the first (oldest) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ - void getFirstRecord() { - if (recordAccessControlPointCharacteristic == null) - return; - - clear(); - mCallbacks.onOperationStarted(getBluetoothDevice()); - recordAccessRequestInProgress = true; - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportFirstStoredRecord()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends abort operation signal to the device. - */ - void abort() { - if (recordAccessControlPointCharacteristic == null) - return; - - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.abortOperation()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends the request to obtain all records from glucose device. Initially we want to notify the - * user about the number of the records so the Report Number of Stored Records request is send. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ - void getAllRecords() { - if (recordAccessControlPointCharacteristic == null) - return; - - clear(); - mCallbacks.onOperationStarted(getBluetoothDevice()); - recordAccessRequestInProgress = true; - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportNumberOfAllStoredRecords()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends the request to obtain all records from glucose device. Initially we want to notify the - * user about the number of the records so the Report Number of Stored Records request is send. - * The data will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ - void refreshRecords() { - if (recordAccessControlPointCharacteristic == null) - return; - - if (records.size() == 0) { - getAllRecords(); - } else { - mCallbacks.onOperationStarted(getBluetoothDevice()); - - // Obtain the last sequence number - final int sequenceNumber = records.keyAt(records.size() - 1) + 1; - recordAccessRequestInProgress = true; - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - // Info: - // Operators OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by the CGMS sample from SDK - // The "Operation not supported" response will be received - } - } - - /** - * Sends the request to remove all stored records from the Continuous Glucose Monitor device. - * This feature is not supported by the CGMS sample from the SDK, so monitor will answer with - * the Op Code Not Supported error. - */ - void deleteAllRecords() { - if (recordAccessControlPointCharacteristic == null) - return; - - clear(); - mCallbacks.onOperationStarted(getBluetoothDevice()); - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.deleteAllStoredRecords()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } -} - diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManagerCallbacks.java deleted file mode 100644 index 4e4eec28..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMManagerCallbacks.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2016, 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.cgm; - -import android.bluetooth.BluetoothDevice; -import androidx.annotation.NonNull; - -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -interface CGMManagerCallbacks extends BatteryManagerCallbacks { - - void onCGMValueReceived(@NonNull final BluetoothDevice device, @NonNull final CGMRecord record); - - void onOperationStarted(final @NonNull BluetoothDevice device); - - void onOperationCompleted(final @NonNull BluetoothDevice device); - - void onOperationFailed(final @NonNull BluetoothDevice device); - - void onOperationAborted(final @NonNull BluetoothDevice device); - - void onOperationNotSupported(final @NonNull BluetoothDevice device); - - void onDataSetCleared(final @NonNull BluetoothDevice device); - - void onNumberOfRecordsRequested(final @NonNull BluetoothDevice device, final int value); - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecord.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecord.java deleted file mode 100644 index 3be66f7d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecord.java +++ /dev/null @@ -1,49 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.cgm; - -import android.os.Parcel; -import android.os.Parcelable; - -class CGMRecord implements Parcelable{ - /** Record sequence number. */ - int sequenceNumber; - /** The base time of the measurement (start time + sequenceNumber of minutes). */ - long timestamp; - /** The glucose concentration in mg/dL. */ - float glucoseConcentration; - - CGMRecord(final int sequenceNumber, final float glucoseConcentration, final long timestamp) { - this.sequenceNumber = sequenceNumber; - this.glucoseConcentration = glucoseConcentration; - this.timestamp = timestamp; - } - - private CGMRecord(final Parcel in) { - this.sequenceNumber = in.readInt(); - this.glucoseConcentration = in.readFloat(); - this.timestamp = in.readLong(); - } - - public static final Creator CREATOR = new Creator() { - @Override - public CGMRecord createFromParcel(final Parcel in) { - return new CGMRecord(in); - } - - @Override - public CGMRecord[] newArray(final int size) { - return new CGMRecord[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(final Parcel parcel, final int flags) { - parcel.writeInt(sequenceNumber); - parcel.writeFloat(glucoseConcentration); - parcel.writeLong(timestamp); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecordsAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecordsAdapter.java deleted file mode 100644 index dc1c0624..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMRecordsAdapter.java +++ /dev/null @@ -1,82 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.cgm; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import no.nordicsemi.android.nrftoolbox.R; - -class CGMRecordsAdapter extends BaseAdapter { - private final static SimpleDateFormat timeFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US); - - private List records; - private LayoutInflater inflater; - - CGMRecordsAdapter(@NonNull final Context context) { - records = new ArrayList<>(); - inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - @Override - public int getCount() { - return records.size(); - } - - @Override - public Object getItem(final int i) { - return null; - } - - @Override - public long getItemId(final int i) { - return i; - } - - @Override - public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) { - ViewHolder viewHolder; - View view = convertView; - if (view == null) { - view = inflater.inflate(R.layout.activity_feature_cgms_item, parent, false); - viewHolder = new ViewHolder(); - viewHolder.concentration = view.findViewById(R.id.cgms_concentration); - viewHolder.time = view.findViewById(R.id.time); - viewHolder.details = view.findViewById(R.id.details); - view.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) view.getTag(); - } - - final CGMRecord CGMRecord = records.get(position); - viewHolder.concentration.setText(String.valueOf(CGMRecord.glucoseConcentration)); - viewHolder.details.setText(viewHolder.details.getResources().getString(R.string.cgms_details, CGMRecord.sequenceNumber)); - viewHolder.time.setText(timeFormat.format(new Date(CGMRecord.timestamp))); - - return view; - } - - void addItem(final CGMRecord record) { - records.add(record); - } - - void clear() { - records.clear(); - } - - private static class ViewHolder { - TextView time; - TextView details; - TextView concentration; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMSActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMSActivity.java deleted file mode 100644 index 0feb0948..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMSActivity.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (c) 2016, 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.cgm; - -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.util.SparseArray; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.ListView; -import android.widget.PopupMenu; -import android.widget.TextView; - -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; -import no.nordicsemi.android.nrftoolbox.proximity.ProximityService; - -public class CGMSActivity extends BleProfileServiceReadyActivity implements PopupMenu.OnMenuItemClickListener { - private View controlPanelStd; - private View controlPanelAbort; - private ListView recordsListView; - private TextView batteryLevelView; - private CGMRecordsAdapter CGMRecordsAdapter; - - private CGMService.CGMSBinder binder; - - @Override - protected void onCreateView(Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_cgms); - setGUI(); - } - - @Override - protected void onInitialize(Bundle savedInstanceState) { - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, makeIntentFilter()); - } - - private void setGUI() { - recordsListView = findViewById(R.id.list); - controlPanelStd = findViewById(R.id.cgms_control_std); - controlPanelAbort = findViewById(R.id.cgms_control_abort); - batteryLevelView = findViewById(R.id.battery); - - findViewById(R.id.action_last).setOnClickListener(v -> { - clearRecords(); - if (binder != null) { - binder.clear(); - binder.getLastRecord(); - } - }); - findViewById(R.id.action_all).setOnClickListener(v -> { - clearRecords(); - if (binder != null) { - clearRecords(); - binder.getAllRecords(); - } - }); - findViewById(R.id.action_abort).setOnClickListener(v -> { - if (binder != null) { - binder.abort(); - } - }); - - // create popup menu attached to the button More - findViewById(R.id.action_more).setOnClickListener(v -> { - PopupMenu menu = new PopupMenu(CGMSActivity.this, v); - menu.setOnMenuItemClickListener(CGMSActivity.this); - MenuInflater inflater = menu.getMenuInflater(); - inflater.inflate(R.menu.gls_more, menu.getMenu()); - menu.show(); - }); - } - - private void loadAdapter(SparseArray records) { - CGMRecordsAdapter.clear(); - for (int i = 0; i < records.size(); i++) { - CGMRecordsAdapter.addItem(records.valueAt(i)); - } - CGMRecordsAdapter.notifyDataSetChanged(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); - } - - @Override - protected void onServiceBound(final CGMService.CGMSBinder binder) { - this.binder = binder; - final SparseArray cgmsRecords = binder.getRecords(); - if (cgmsRecords != null && cgmsRecords.size() > 0) { - if (CGMRecordsAdapter == null) { - CGMRecordsAdapter = new CGMRecordsAdapter(CGMSActivity.this); - recordsListView.setAdapter(CGMRecordsAdapter); - } - loadAdapter(cgmsRecords); - } - } - - @Override - protected void onServiceUnbound() { - binder = null; - } - - @Override - protected Class getServiceClass() { - return CGMService.class; - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.cgms_feature_title; - } - - @Override - protected int getAboutTextId() { - return R.string.cgms_about_text; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.cgms_default_name; - } - - @Override - protected UUID getFilterUUID() { - return CGMManager.CGMS_UUID; - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // this may notify user or show some views - } - - private void setOperationInProgress(final boolean progress) { - runOnUiThread(() -> { - // setSupportProgressBarIndeterminateVisibility(progress); - controlPanelStd.setVisibility(!progress ? View.VISIBLE : View.GONE); - controlPanelAbort.setVisibility(progress ? View.VISIBLE : View.GONE); - }); - } - - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int value) { - batteryLevelView.setText(getString(R.string.battery, value)); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - setOperationInProgress(false); - batteryLevelView.setText(R.string.not_available); - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - super.onError(device, message, errorCode); - setOperationInProgress(false); - } - - @Override - protected void setDefaultUI() { - clearRecords(); - batteryLevelView.setText(R.string.not_available); - } - - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - switch (menuItem.getItemId()) { - case R.id.action_refresh: - if(binder != null) - binder.refreshRecords(); - break; - case R.id.action_first: - if (binder != null) - binder.getFirstRecord(); - break; - case R.id.action_clear: - if (binder != null) - binder.clear(); - break; - case R.id.action_delete_all: - if (binder != null) - binder.deleteAllRecords(); - break; - } - return true; - } - - private void clearRecords() { - if (CGMRecordsAdapter != null) { - CGMRecordsAdapter.clear(); - CGMRecordsAdapter.notifyDataSetChanged(); - } - } - - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - final BluetoothDevice device = intent.getParcelableExtra(ProximityService.EXTRA_DEVICE); - - switch (action) { - case CGMService.BROADCAST_NEW_CGMS_VALUE: { - CGMRecord CGMRecord = intent.getExtras().getParcelable(CGMService.EXTRA_CGMS_RECORD); - if (CGMRecordsAdapter == null) { - CGMRecordsAdapter = new CGMRecordsAdapter(CGMSActivity.this); - recordsListView.setAdapter(CGMRecordsAdapter); - } - CGMRecordsAdapter.addItem(CGMRecord); - CGMRecordsAdapter.notifyDataSetChanged(); - break; - } - case CGMService.BROADCAST_DATA_SET_CLEAR: - // Update GUI - clearRecords(); - break; - case CGMService.OPERATION_STARTED: - // Update GUI - setOperationInProgress(true); - break; - case CGMService.BROADCAST_BATTERY_LEVEL: - final int batteryLevel = intent.getIntExtra(CGMService.EXTRA_BATTERY_LEVEL, 0); - // Update GUI - onBatteryLevelChanged(device, batteryLevel); - break; - case CGMService.OPERATION_FAILED: - // Update GUI - showToast(R.string.gls_operation_failed); - // breakthrough intended - default: - // Update GUI - setOperationInProgress(false); - } - } - }; - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(CGMService.BROADCAST_NEW_CGMS_VALUE); - intentFilter.addAction(CGMService.BROADCAST_DATA_SET_CLEAR); - intentFilter.addAction(CGMService.OPERATION_STARTED); - intentFilter.addAction(CGMService.OPERATION_COMPLETED); - intentFilter.addAction(CGMService.OPERATION_SUPPORTED); - intentFilter.addAction(CGMService.OPERATION_NOT_SUPPORTED); - intentFilter.addAction(CGMService.OPERATION_ABORTED); - intentFilter.addAction(CGMService.OPERATION_FAILED); - intentFilter.addAction(CGMService.BROADCAST_BATTERY_LEVEL); - return intentFilter; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMService.java deleted file mode 100644 index da90c7e9..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/cgm/CGMService.java +++ /dev/null @@ -1,316 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.cgm; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.util.SparseArray; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -public class CGMService extends BleProfileService implements CGMManagerCallbacks { - private static final String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.cgms.ACTION_DISCONNECT"; - - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - - public static final String BROADCAST_NEW_CGMS_VALUE = "no.nordicsemi.android.nrftoolbox.cgms.BROADCAST_NEW_CGMS_VALUE"; - public static final String BROADCAST_DATA_SET_CLEAR = "no.nordicsemi.android.nrftoolbox.cgms.BROADCAST_DATA_SET_CLEAR"; - public static final String OPERATION_STARTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_STARTED"; - public static final String OPERATION_COMPLETED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_COMPLETED"; - public static final String OPERATION_SUPPORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_SUPPORTED"; - public static final String OPERATION_NOT_SUPPORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_NOT_SUPPORTED"; - public static final String OPERATION_FAILED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_FAILED"; - public static final String OPERATION_ABORTED = "no.nordicsemi.android.nrftoolbox.cgms.OPERATION_ABORTED"; - public static final String EXTRA_CGMS_RECORD = "no.nordicsemi.android.nrftoolbox.cgms.EXTRA_CGMS_RECORD"; - public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.cgms.EXTRA_DATA"; - - private final static int NOTIFICATION_ID = 229; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; - - private CGMManager manager; - private final LocalBinder binder = new CGMSBinder(); - - /** - * This local binder is an interface for the bonded activity to operate with the RSC sensor - */ - - class CGMSBinder extends LocalBinder { - /** - * Returns all records as a sparse array where sequence number is the key. - * - * @return the records list - */ - SparseArray getRecords() { - return manager.getRecords(); - } - - /** - * Clears the records list locally - */ - void clear() { - if (manager != null) - manager.clear(); - } - - /** - * Sends the request to obtain the first (oldest) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access Control - * Point indication with status code ({@link CGMManager# RESPONSE_SUCCESS} or other in case of error. - */ - void getFirstRecord() { - if (manager != null) - manager.getFirstRecord(); - } - - /** - * Sends the request to obtain the last (most recent) record from glucose device. - * The data will be returned to Glucose Measurement characteristic as a notification followed by Record Access - * Control Point indication with status code Success or other in case of error. - */ - void getLastRecord() { - if (manager != null) - manager.getLastRecord(); - } - - /** - * Sends the request to obtain all records from glucose device. - * Initially we want to notify user about the number of the records so the Report Number of Stored Records is send. - * The data will be returned to Glucose Measurement characteristic as a series of notifications followed - * by Record Access Control Point indication with status code Success or other in case of error. - */ - void getAllRecords() { - if (manager != null) - manager.getAllRecords(); - } - - /** - * Sends the request to obtain all records from glucose device with sequence number greater - * than the last one already obtained. The data will be returned to Glucose Measurement - * characteristic as a series of notifications followed by Record Access Control Point - * indication with status code Success or other in case of error. - */ - void refreshRecords() { - if (manager != null) - manager.refreshRecords(); - } - - /** - * Sends abort operation signal to the device - */ - void abort() { - if (manager != null) - manager.abort(); - } - - /** - * Sends Delete op code with All stored records parameter. This method may not be supported by the SDK sample. - */ - void deleteAllRecords() { - if (manager != null) - manager.deleteAllRecords(); - } - } - - @Override - protected LocalBinder getBinder() { - return binder; - } - - @Override - protected LoggableBleManager initializeManager() { - return manager = new CGMManager(this); - } - - - @Override - public void onCreate() { - super.onCreate(); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(disconnectActionBroadcastReceiver, filter); - } - - @Override - public void onDestroy() { - // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - stopForegroundService(); - unregisterReceiver(disconnectActionBroadcastReceiver); - - super.onDestroy(); - } - - @Override - protected void onRebind() { - startForegroundService(); - } - - @Override - protected void onUnbind() { - startForegroundService(); - } - - /** - * Sets the service as a foreground service - */ - private void startForegroundService(){ - // when the activity closes we need to show the notification that user is connected to the peripheral sensor - // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services - final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - } - - /** - * Stops the service as a foreground service - */ - private void 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) { - stopForeground(true); - } else { - cancelNotification(); - } - } - - /** - * Creates the notification - * - * @param messageResId the message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults signals that will be used to notify the user - */ - @SuppressWarnings("SameParameterValue") - private Notification createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, CGMSActivity.class); - - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - - // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); - builder.setContentIntent(pendingIntent); - builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); - builder.setSmallIcon(R.drawable.ic_stat_notify_cgms); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction)); - - return builder.build(); - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } - - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver disconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; - - @Override - public void onCGMValueReceived(@NonNull final BluetoothDevice device, @NonNull final CGMRecord record) { - final Intent broadcast = new Intent(BROADCAST_NEW_CGMS_VALUE); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_CGMS_RECORD, record); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onOperationStarted(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_STARTED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onOperationCompleted(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_COMPLETED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onOperationFailed(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_FAILED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onOperationAborted(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_ABORTED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, true); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onOperationNotSupported(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(OPERATION_NOT_SUPPORTED); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, false); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDataSetCleared(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_DATA_SET_CLEAR); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onNumberOfRecordsRequested(@NonNull final BluetoothDevice device, final int value) { - if (value == 0) - showToast(R.string.gls_progress_zero); - else - showToast(getResources().getQuantityString(R.plurals.gls_progress, value, value)); - - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCActivity.java deleted file mode 100644 index 53318478..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCActivity.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright (c) 2015, 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.csc; - -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; - -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.view.Menu; -import android.widget.TextView; - -import java.util.Locale; -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.csc.settings.SettingsActivity; -import no.nordicsemi.android.nrftoolbox.csc.settings.SettingsFragment; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; - -public class CSCActivity extends BleProfileServiceReadyActivity { - private TextView speedView; - private TextView speedUnitView; - private TextView cadenceView; - private TextView distanceView; - private TextView distanceUnitView; - private TextView totalDistanceView; - private TextView totalDistanceUnitView; - private TextView gearRatioView; - private TextView batteryLevelView; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_csc); - setGui(); - } - - @Override - protected void onInitialize(final Bundle savedInstanceState) { - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, makeIntentFilter()); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); - } - - private void setGui() { - speedView = findViewById(R.id.speed); - speedUnitView = findViewById(R.id.speed_unit); - cadenceView = findViewById(R.id.cadence); - distanceView = findViewById(R.id.distance); - distanceUnitView = findViewById(R.id.distance_unit); - totalDistanceView = findViewById(R.id.distance_total); - totalDistanceUnitView = findViewById(R.id.distance_total_unit); - gearRatioView = findViewById(R.id.ratio); - batteryLevelView = findViewById(R.id.battery); - } - - @Override - protected void onResume() { - super.onResume(); - setDefaultUI(); - } - - @Override - protected void setDefaultUI() { - speedView.setText(R.string.not_available_value); - cadenceView.setText(R.string.not_available_value); - distanceView.setText(R.string.not_available_value); - totalDistanceView.setText(R.string.not_available_value); - gearRatioView.setText(R.string.not_available_value); - batteryLevelView.setText(R.string.not_available); - - setUnits(); - } - - private void setUnits() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT))); - - switch (unit) { - case SettingsFragment.SETTINGS_UNIT_M_S: // [m/s] - speedUnitView.setText(R.string.csc_speed_unit_m_s); - distanceUnitView.setText(R.string.csc_distance_unit_m); - totalDistanceUnitView.setText(R.string.csc_total_distance_unit_km); - break; - case SettingsFragment.SETTINGS_UNIT_KM_H: // [km/h] - speedUnitView.setText(R.string.csc_speed_unit_km_h); - distanceUnitView.setText(R.string.csc_distance_unit_m); - totalDistanceUnitView.setText(R.string.csc_total_distance_unit_km); - break; - case SettingsFragment.SETTINGS_UNIT_MPH: // [mph] - speedUnitView.setText(R.string.csc_speed_unit_mph); - distanceUnitView.setText(R.string.csc_distance_unit_yd); - totalDistanceUnitView.setText(R.string.csc_total_distance_unit_mile); - break; - } - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.csc_feature_title; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.csc_default_name; - } - - @Override - protected int getAboutTextId() { - return R.string.csc_about_text; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.settings_and_about, menu); - return true; - } - - @Override - protected boolean onOptionsItemSelected(final int itemId) { - switch (itemId) { - case R.id.action_settings: - final Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - break; - } - return true; - } - - @Override - protected Class getServiceClass() { - return CSCService.class; - } - - @Override - protected UUID getFilterUUID() { - return CSCManager.CYCLING_SPEED_AND_CADENCE_SERVICE_UUID; - } - - @Override - protected void onServiceBound(final CSCService.CSCBinder binder) { - // not used - } - - @Override - protected void onServiceUnbound() { - // not used - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // not used - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - batteryLevelView.setText(R.string.not_available); - } - - private void onMeasurementReceived(final BluetoothDevice device, float speed, float distance, float totalDistance) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT))); - - switch (unit) { - case SettingsFragment.SETTINGS_UNIT_KM_H: - speed = speed * 3.6f; - // pass through intended - case SettingsFragment.SETTINGS_UNIT_M_S: - if (distance < 1000) { // 1 km in m - distanceView.setText(String.format(Locale.US, "%.0f", distance)); - distanceUnitView.setText(R.string.csc_distance_unit_m); - } else { - distanceView.setText(String.format(Locale.US, "%.2f", distance / 1000.0f)); - distanceUnitView.setText(R.string.csc_distance_unit_km); - } - - totalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1000.0f)); - break; - case SettingsFragment.SETTINGS_UNIT_MPH: - speed = speed * 2.2369f; - if (distance < 1760) { // 1 mile in yrs - distanceView.setText(String.format(Locale.US, "%.0f", distance)); - distanceUnitView.setText(R.string.csc_distance_unit_yd); - } else { - distanceView.setText(String.format(Locale.US, "%.2f", distance / 1760.0f)); - distanceUnitView.setText(R.string.csc_distance_unit_mile); - } - - totalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1609.31f)); - break; - } - - speedView.setText(String.format(Locale.US, "%.1f", speed)); - } - - private void onGearRatioUpdate(final BluetoothDevice device, final int cadence, final float ratio) { - cadenceView.setText(String.format(Locale.US, "%d", cadence)); - gearRatioView.setText(String.format(Locale.US, "%.1f", ratio)); - } - - public void onBatteryLevelChanged(final BluetoothDevice device, final int value) { - batteryLevelView.setText(getString(R.string.battery, value)); - } - - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - final BluetoothDevice device = intent.getParcelableExtra(CSCService.EXTRA_DEVICE); - - if (CSCService.BROADCAST_WHEEL_DATA.equals(action)) { - final float speed = intent.getFloatExtra(CSCService.EXTRA_SPEED, 0.0f); // [m/s] - final float distance = intent.getFloatExtra(CSCService.EXTRA_DISTANCE, 0); - final float totalDistance = intent.getFloatExtra(CSCService.EXTRA_TOTAL_DISTANCE, 0); - // Update GUI - onMeasurementReceived(device, speed, distance, totalDistance); - } else if (CSCService.BROADCAST_CRANK_DATA.equals(action)) { - final float ratio = intent.getFloatExtra(CSCService.EXTRA_GEAR_RATIO, 0); - final int cadence = intent.getIntExtra(CSCService.EXTRA_CADENCE, 0); - // Update GUI - onGearRatioUpdate(device, cadence, ratio); - } else if (CSCService.BROADCAST_BATTERY_LEVEL.equals(action)) { - final int batteryLevel = intent.getIntExtra(CSCService.EXTRA_BATTERY_LEVEL, 0); - // Update GUI - onBatteryLevelChanged(device, batteryLevel); - } - } - }; - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(CSCService.BROADCAST_WHEEL_DATA); - intentFilter.addAction(CSCService.BROADCAST_CRANK_DATA); - intentFilter.addAction(CSCService.BROADCAST_BATTERY_LEVEL); - return intentFilter; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManager.java deleted file mode 100644 index 239de234..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManager.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2015, 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.csc; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - -import androidx.annotation.FloatRange; -import androidx.annotation.NonNull; -import android.util.Log; - -import java.util.UUID; - -import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.csc.settings.SettingsFragment; -import no.nordicsemi.android.nrftoolbox.parser.CSCMeasurementParser; - -public class CSCManager extends BatteryManager { - /** Cycling Speed and Cadence service UUID. */ - final static UUID CYCLING_SPEED_AND_CADENCE_SERVICE_UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb"); - /** Cycling Speed and Cadence Measurement characteristic UUID. */ - private final static UUID CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb"); - - private final SharedPreferences preferences; - private BluetoothGattCharacteristic cscMeasurementCharacteristic; - - CSCManager(final Context context) { - super(context); - preferences = PreferenceManager.getDefaultSharedPreferences(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new CSCManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ - private class CSCManagerGattCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - super.initialize(); - - // CSC characteristic is required - setNotificationCallback(cscMeasurementCharacteristic) - .with(new CyclingSpeedAndCadenceMeasurementDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, final @NonNull Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + CSCMeasurementParser.parse(data) + "\" received"); - - // Pass through received data - super.onDataReceived(device, data); - } - - @Override - public float getWheelCircumference() { - return Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_WHEEL_SIZE, - String.valueOf(SettingsFragment.SETTINGS_WHEEL_SIZE_DEFAULT))); - } - - @Override - public void onDistanceChanged(@NonNull final BluetoothDevice device, - @FloatRange(from = 0) final float totalDistance, - @FloatRange(from = 0) final float distance, - @FloatRange(from = 0) final float speed) { - mCallbacks.onDistanceChanged(device, totalDistance, distance, speed); - } - - @Override - public void onCrankDataChanged(@NonNull final BluetoothDevice device, - @FloatRange(from = 0) final float crankCadence, - final float gearRatio) { - mCallbacks.onCrankDataChanged(device, crankCadence, gearRatio); - } - - @Override - public void onInvalidDataReceived(@NonNull final BluetoothDevice device, - @NonNull final Data data) { - log(Log.WARN, "Invalid CSC Measurement data received: " + data); - } - }); - enableNotifications(cscMeasurementCharacteristic).enqueue(); - } - - @Override - public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(CYCLING_SPEED_AND_CADENCE_SERVICE_UUID); - if (service != null) { - cscMeasurementCharacteristic = service.getCharacteristic(CSC_MEASUREMENT_CHARACTERISTIC_UUID); - } - return cscMeasurementCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - super.onDeviceDisconnected(); - cscMeasurementCharacteristic = null; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManagerCallbacks.java deleted file mode 100644 index 12035da5..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCManagerCallbacks.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2015, 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.csc; - -import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -interface CSCManagerCallbacks extends BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback { -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java deleted file mode 100644 index 8f57164c..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/CSCService.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) 2015, 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.csc; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -public class CSCService extends BleProfileService implements CSCManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "CSCService"; - - public static final String BROADCAST_WHEEL_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_WHEEL_DATA"; - /** - * Speed in meters per second. - */ - public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_SPEED"; - /** - * Distance in meters. - */ - public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_DISTANCE"; - /** - * Total distance in meters. - */ - public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_TOTAL_DISTANCE"; - - public static final String BROADCAST_CRANK_DATA = "no.nordicsemi.android.nrftoolbox.csc.BROADCAST_CRANK_DATA"; - public static final String EXTRA_GEAR_RATIO = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_GEAR_RATIO"; - public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.csc.EXTRA_CADENCE"; - - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - - private static final String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.csc.ACTION_DISCONNECT"; - - private final static int NOTIFICATION_ID = 200; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; - - private final LocalBinder binder = new CSCBinder(); - private CSCManager manager; - - /** - * This local binder is an interface for the bonded activity to operate with the RSC sensor - */ - class CSCBinder extends LocalBinder { - // empty - } - - @Override - protected LocalBinder getBinder() { - return binder; - } - - @Override - protected LoggableBleManager initializeManager() { - return manager = new CSCManager(this); - } - - @Override - public void onCreate() { - super.onCreate(); - - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(disconnectActionBroadcastReceiver, filter); - } - - @Override - public void onDestroy() { - // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - cancelNotification(); - unregisterReceiver(disconnectActionBroadcastReceiver); - - super.onDestroy(); - } - - @Override - protected void onRebind() { - stopForegroundService(); - - if (isConnected()) { - // This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property). - // If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications. - manager.readBatteryLevelCharacteristic(); - } - } - - @Override - protected void onUnbind() { - // When we are connected, but the application is not open, we are not really interested in battery level notifications. - // But we will still be receiving other values, if enabled. - if (isConnected()) - manager.disableBatteryLevelCharacteristicNotifications(); - startForegroundService(); - } - - @Override - public void onDistanceChanged(@NonNull final BluetoothDevice device, final float totalDistance, final float distance, final float speed) { - final Intent broadcast = new Intent(BROADCAST_WHEEL_DATA); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_SPEED, speed); - broadcast.putExtra(EXTRA_DISTANCE, distance); - broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onCrankDataChanged(@NonNull final BluetoothDevice device, final float crankCadence, final float gearRatio) { - final Intent broadcast = new Intent(BROADCAST_CRANK_DATA); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_GEAR_RATIO, gearRatio); - broadcast.putExtra(EXTRA_CADENCE, (int) crankCadence); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Sets the service as a foreground service - */ - private void startForegroundService(){ - // when the activity closes we need to show the notification that user is connected to the peripheral sensor - // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services - final Notification notification = createNotification(R.string.csc_notification_connected_message, 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - } - - /** - * Stops the service as a foreground service - */ - private void 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) { - stopForeground(true); - } else { - cancelNotification(); - } - } - - /** - * Creates the notification - * - * @param messageResId the message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - */ - @SuppressWarnings("SameParameterValue") - private Notification createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, CSCActivity.class); - - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - - // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); - builder.setContentIntent(pendingIntent); - builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); - builder.setSmallIcon(R.drawable.ic_stat_notify_csc); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.csc_notification_action_disconnect), disconnectAction)); - - return builder.build(); - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } - - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver disconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsActivity.java deleted file mode 100644 index ebbf5125..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsActivity.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2015, 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.csc.settings; - -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.MenuItem; - -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsActivity extends AppCompatActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Display the fragment as the main content. - getSupportFragmentManager().beginTransaction().replace(R.id.content, new SettingsFragment()).commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsFragment.java deleted file mode 100644 index 2a3b41bb..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/csc/settings/SettingsFragment.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2015, 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.csc.settings; - -import android.content.SharedPreferences; -import android.os.Bundle; -import androidx.preference.PreferenceScreen; - -import androidx.preference.PreferenceFragmentCompat; -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsFragment extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { - public static final String SETTINGS_WHEEL_SIZE = "settings_wheel_size"; - public static final int SETTINGS_WHEEL_SIZE_DEFAULT = 2340; - public static final String SETTINGS_UNIT = "settings_csc_unit"; - public static final int SETTINGS_UNIT_M_S = 0; // [m/s] - public static final int SETTINGS_UNIT_KM_H = 1; // [m/s] - public static final int SETTINGS_UNIT_MPH = 2; // [m/s] - public static final int SETTINGS_UNIT_DEFAULT = SETTINGS_UNIT_KM_H; - - @Override - public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.settings_csc); - - // set initial values - updateWheelSizeSummary(); - } - - @Override - public void onResume() { - super.onResume(); - - // attach the preference change listener. It will update the summary below interval preference - getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onPause() { - super.onPause(); - - // unregister listener - getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { - if (SETTINGS_WHEEL_SIZE.equals(key)) { - updateWheelSizeSummary(); - } - } - - private void updateWheelSizeSummary() { - final PreferenceScreen screen = getPreferenceScreen(); - final SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); - - final String value = preferences.getString(SETTINGS_WHEEL_SIZE, String.valueOf(SETTINGS_WHEEL_SIZE_DEFAULT)); - screen.findPreference(SETTINGS_WHEEL_SIZE).setSummary(getString(R.string.csc_settings_wheel_diameter_summary, value)); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java deleted file mode 100644 index 40647ca1..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuActivity.java +++ /dev/null @@ -1,843 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu; - -import android.app.ActivityManager; -import android.app.ActivityManager.RunningServiceInfo; -import android.app.LoaderManager.LoaderCallbacks; -import android.app.NotificationManager; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.Context; -import android.content.CursorLoader; -import android.content.Intent; -import android.content.Loader; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.provider.MediaStore; -import android.text.TextUtils; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.webkit.MimeTypeMap; -import android.widget.Button; -import android.widget.ListView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import java.io.File; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.dfu.DfuProgressListener; -import no.nordicsemi.android.dfu.DfuProgressListenerAdapter; -import no.nordicsemi.android.dfu.DfuServiceInitiator; -import no.nordicsemi.android.dfu.DfuServiceListenerHelper; -import no.nordicsemi.android.nrftoolbox.AppHelpFragment; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.dfu.adapter.FileBrowserAppsAdapter; -import no.nordicsemi.android.nrftoolbox.dfu.fragment.UploadCancelFragment; -import no.nordicsemi.android.nrftoolbox.dfu.fragment.ZipInfoFragment; -import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsActivity; -import no.nordicsemi.android.nrftoolbox.dfu.settings.SettingsFragment; -import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; - -/** - * DfuActivity is the main DFU activity It implements DFUManagerCallbacks to receive callbacks from - * DfuManager class It implements DeviceScannerFragment.OnDeviceSelectedListener callback to receive callback when device is selected from scanning dialog The activity supports portrait and - * landscape orientations - */ -public class DfuActivity extends AppCompatActivity implements LoaderCallbacks, ScannerFragment.OnDeviceSelectedListener, - UploadCancelFragment.CancelFragmentListener { - private static final String TAG = "DfuActivity"; - - private static final String PREFS_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_DEVICE_NAME"; - private static final String PREFS_FILE_NAME = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_NAME"; - private static final String PREFS_FILE_TYPE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_TYPE"; - private static final String PREFS_FILE_SCOPE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_SCOPE"; - private static final String PREFS_FILE_SIZE = "no.nordicsemi.android.nrftoolbox.dfu.PREFS_FILE_SIZE"; - - private static final String DATA_DEVICE = "device"; - private static final String DATA_FILE_TYPE = "file_type"; - private static final String DATA_FILE_TYPE_TMP = "file_type_tmp"; - private static final String DATA_FILE_PATH = "file_path"; - private static final String DATA_FILE_STREAM = "file_stream"; - private static final String DATA_INIT_FILE_PATH = "init_file_path"; - private static final String DATA_INIT_FILE_STREAM = "init_file_stream"; - private static final String DATA_STATUS = "status"; - private static final String DATA_SCOPE = "scope"; - private static final String DATA_DFU_COMPLETED = "dfu_completed"; - private static final String DATA_DFU_ERROR = "dfu_error"; - - private static final String EXTRA_URI = "uri"; - - private static final int ENABLE_BT_REQ = 0; - private static final int SELECT_FILE_REQ = 1; - private static final int SELECT_INIT_FILE_REQ = 2; - - private TextView deviceNameView; - private TextView fileNameView; - private TextView fileTypeView; - private TextView fileScopeView; - private TextView fileSizeView; - private TextView fileStatusView; - private TextView textPercentage; - private TextView textUploading; - private ProgressBar progressBar; - - private Button selectFileButton, uploadButton, connectButton; - - private BluetoothDevice selectedDevice; - private String filePath; - private Uri fileStreamUri; - private String initFilePath; - private Uri initFileStreamUri; - private int fileType; - private int fileTypeTmp; // This value is being used when user is selecting a file not to overwrite the old value (in case he/she will cancel selecting file) - private Integer scope; - private boolean statusOk; - /** Flag set to true in {@link #onRestart()} and to false in {@link #onPause()}. */ - private boolean resumed; - /** Flag set to true if DFU operation was completed while {@link #resumed} was false. */ - private boolean dfuCompleted; - /** The error message received from DFU service while {@link #resumed} was false. */ - private String dfuError; - - /** - * The progress listener receives events from the DFU Service. - * If is registered in onCreate() and unregistered in onDestroy() so methods here may also be called - * when the screen is locked or the app went to the background. This is because the UI needs to have the - * correct information after user comes back to the activity and this information can't be read from the service - * as it might have been killed already (DFU completed or finished with error). - */ - private final DfuProgressListener dfuProgressListener = new DfuProgressListenerAdapter() { - @Override - public void onDeviceConnecting(@NonNull final String deviceAddress) { - progressBar.setIndeterminate(true); - textPercentage.setText(R.string.dfu_status_connecting); - } - - @Override - public void onDfuProcessStarting(@NonNull final String deviceAddress) { - progressBar.setIndeterminate(true); - textPercentage.setText(R.string.dfu_status_starting); - } - - @Override - public void onEnablingDfuMode(@NonNull final String deviceAddress) { - progressBar.setIndeterminate(true); - textPercentage.setText(R.string.dfu_status_switching_to_dfu); - } - - @Override - public void onFirmwareValidating(@NonNull final String deviceAddress) { - progressBar.setIndeterminate(true); - textPercentage.setText(R.string.dfu_status_validating); - } - - @Override - public void onDeviceDisconnecting(@NonNull final String deviceAddress) { - progressBar.setIndeterminate(true); - textPercentage.setText(R.string.dfu_status_disconnecting); - } - - @Override - public void onDfuCompleted(@NonNull final String deviceAddress) { - textPercentage.setText(R.string.dfu_status_completed); - if (resumed) { - // let's wait a bit until we cancel the notification. When canceled immediately it will be recreated by service again. - new Handler().postDelayed(() -> { - onTransferCompleted(); - - // if this activity is still open and upload process was completed, cancel the notification - final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.cancel(DfuService.NOTIFICATION_ID); - }, 200); - } else { - // Save that the DFU process has finished - dfuCompleted = true; - } - } - - @Override - public void onDfuAborted(@NonNull final String deviceAddress) { - textPercentage.setText(R.string.dfu_status_aborted); - // let's wait a bit until we cancel the notification. When canceled immediately it will be recreated by service again. - new Handler().postDelayed(() -> { - onUploadCanceled(); - - // if this activity is still open and upload process was completed, cancel the notification - final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.cancel(DfuService.NOTIFICATION_ID); - }, 200); - } - - @Override - public void onProgressChanged(@NonNull final String deviceAddress, final int percent, - final float speed, final float avgSpeed, - final int currentPart, final int partsTotal) { - progressBar.setIndeterminate(false); - progressBar.setProgress(percent); - textPercentage.setText(getString(R.string.dfu_uploading_percentage, percent)); - if (partsTotal > 1) - textUploading.setText(getString(R.string.dfu_status_uploading_part, currentPart, partsTotal)); - else - textUploading.setText(R.string.dfu_status_uploading); - } - - @Override - public void onError(@NonNull final String deviceAddress, final int error, final int errorType, final String message) { - if (resumed) { - showErrorMessage(message); - - // We have to wait a bit before canceling notification. This is called before DfuService creates the last notification. - new Handler().postDelayed(() -> { - // if this activity is still open and upload process was completed, cancel the notification - final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.cancel(DfuService.NOTIFICATION_ID); - }, 200); - } else { - dfuError = message; - } - } - }; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_feature_dfu); - isBLESupported(); - if (!isBLEEnabled()) { - showBLEDialog(); - } - setGUI(); - - // restore saved state - fileType = DfuService.TYPE_AUTO; // Default - if (savedInstanceState != null) { - fileType = savedInstanceState.getInt(DATA_FILE_TYPE); - fileTypeTmp = savedInstanceState.getInt(DATA_FILE_TYPE_TMP); - filePath = savedInstanceState.getString(DATA_FILE_PATH); - fileStreamUri = savedInstanceState.getParcelable(DATA_FILE_STREAM); - initFilePath = savedInstanceState.getString(DATA_INIT_FILE_PATH); - initFileStreamUri = savedInstanceState.getParcelable(DATA_INIT_FILE_STREAM); - selectedDevice = savedInstanceState.getParcelable(DATA_DEVICE); - statusOk = statusOk || savedInstanceState.getBoolean(DATA_STATUS); - scope = savedInstanceState.containsKey(DATA_SCOPE) ? savedInstanceState.getInt(DATA_SCOPE) : null; - uploadButton.setEnabled(selectedDevice != null && statusOk); - dfuCompleted = savedInstanceState.getBoolean(DATA_DFU_COMPLETED); - dfuError = savedInstanceState.getString(DATA_DFU_ERROR); - } - - DfuServiceListenerHelper.registerProgressListener(this, dfuProgressListener); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - DfuServiceListenerHelper.unregisterProgressListener(this, dfuProgressListener); - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - outState.putInt(DATA_FILE_TYPE, fileType); - outState.putInt(DATA_FILE_TYPE_TMP, fileTypeTmp); - outState.putString(DATA_FILE_PATH, filePath); - outState.putParcelable(DATA_FILE_STREAM, fileStreamUri); - outState.putString(DATA_INIT_FILE_PATH, initFilePath); - outState.putParcelable(DATA_INIT_FILE_STREAM, initFileStreamUri); - outState.putParcelable(DATA_DEVICE, selectedDevice); - outState.putBoolean(DATA_STATUS, statusOk); - if (scope != null) outState.putInt(DATA_SCOPE, scope); - outState.putBoolean(DATA_DFU_COMPLETED, dfuCompleted); - outState.putString(DATA_DFU_ERROR, dfuError); - } - - private void setGUI() { - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - deviceNameView = findViewById(R.id.device_name); - fileNameView = findViewById(R.id.file_name); - fileTypeView = findViewById(R.id.file_type); - fileScopeView = findViewById(R.id.file_scope); - fileSizeView = findViewById(R.id.file_size); - fileStatusView = findViewById(R.id.file_status); - selectFileButton = findViewById(R.id.action_select_file); - uploadButton = findViewById(R.id.action_upload); - connectButton = findViewById(R.id.action_connect); - textPercentage = findViewById(R.id.textviewProgress); - textUploading = findViewById(R.id.textviewUploading); - progressBar = findViewById(R.id.progressbar_file); - - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - if (isDfuServiceRunning()) { - // Restore image file information - deviceNameView.setText(preferences.getString(PREFS_DEVICE_NAME, "")); - fileNameView.setText(preferences.getString(PREFS_FILE_NAME, "")); - fileTypeView.setText(preferences.getString(PREFS_FILE_TYPE, "")); - fileScopeView.setText(preferences.getString(PREFS_FILE_SCOPE, "")); - fileSizeView.setText(preferences.getString(PREFS_FILE_SIZE, "")); - fileStatusView.setText(R.string.dfu_file_status_ok); - statusOk = true; - showProgressBar(); - } - } - - @Override - protected void onResume() { - super.onResume(); - resumed = true; - if (dfuCompleted) - onTransferCompleted(); - if (dfuError != null) - showErrorMessage(dfuError); - if (dfuCompleted || dfuError != null) { - // if this activity is still open and upload process was completed, cancel the notification - final NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.cancel(DfuService.NOTIFICATION_ID); - dfuCompleted = false; - dfuError = null; - } - } - - @Override - protected void onPause() { - super.onPause(); - resumed = false; - } - - private void isBLESupported() { - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { - showToast(R.string.no_ble); - finish(); - } - } - - private boolean isBLEEnabled() { - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - return adapter != null && adapter.isEnabled(); - } - - private void showBLEDialog() { - final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult(enableIntent, ENABLE_BT_REQ); - } - - private void showDeviceScanningDialog() { - final ScannerFragment dialog = ScannerFragment.getInstance(null); // Device that is advertising directly does not have the GENERAL_DISCOVERABLE nor LIMITED_DISCOVERABLE flag set. - dialog.show(getSupportFragmentManager(), "scan_fragment"); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.settings_and_about, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - break; - case R.id.action_about: - final AppHelpFragment fragment = AppHelpFragment.getInstance(R.string.dfu_about_text); - fragment.show(getSupportFragmentManager(), "help_fragment"); - break; - case R.id.action_settings: - final Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - break; - } - return true; - } - - @Override - protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode != RESULT_OK) - return; - - switch (requestCode) { - case SELECT_FILE_REQ: { - // clear previous data - fileType = fileTypeTmp; - filePath = null; - fileStreamUri = null; - - // and read new one - final Uri uri = data.getData(); - /* - * The URI returned from application may be in 'file' or 'content' schema. 'File' schema allows us to create a File object and read details from if - * directly. Data from 'Content' schema must be read by Content Provider. To do that we are using a Loader. - */ - if (uri.getScheme().equals("file")) { - // the direct path to the file has been returned - final String path = uri.getPath(); - final File file = new File(path); - filePath = path; - - updateFileInfo(file.getName(), file.length(), fileType); - } else if (uri.getScheme().equals("content")) { - // an Uri has been returned - fileStreamUri = uri; - // if application returned Uri for streaming, let's us it. Does it works? - // FIXME both Uris works with Google Drive app. Why both? What's the difference? How about other apps like DropBox? - final Bundle extras = data.getExtras(); - if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) - fileStreamUri = extras.getParcelable(Intent.EXTRA_STREAM); - - // file name and size must be obtained from Content Provider - final Bundle bundle = new Bundle(); - bundle.putParcelable(EXTRA_URI, uri); - getLoaderManager().restartLoader(SELECT_FILE_REQ, bundle, this); - } - break; - } - case SELECT_INIT_FILE_REQ: { - initFilePath = null; - initFileStreamUri = null; - - // and read new one - final Uri uri = data.getData(); - /* - * The URI returned from application may be in 'file' or 'content' schema. 'File' schema allows us to create a File object and read details from if - * directly. Data from 'Content' schema must be read by Content Provider. To do that we are using a Loader. - */ - if (uri.getScheme().equals("file")) { - // the direct path to the file has been returned - initFilePath = uri.getPath(); - fileStatusView.setText(R.string.dfu_file_status_ok_with_init); - } else if (uri.getScheme().equals("content")) { - // an Uri has been returned - initFileStreamUri = uri; - // if application returned Uri for streaming, let's us it. Does it works? - // FIXME both Uris works with Google Drive app. Why both? What's the difference? How about other apps like DropBox? - final Bundle extras = data.getExtras(); - if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) - initFileStreamUri = extras.getParcelable(Intent.EXTRA_STREAM); - fileStatusView.setText(R.string.dfu_file_status_ok_with_init); - } - break; - } - default: - break; - } - } - - @Override - public Loader onCreateLoader(final int id, final Bundle args) { - final Uri uri = args.getParcelable(EXTRA_URI); - /* - * Some apps, f.e. Google Drive allow to select file that is not on the device. There is no "_data" column handled by that provider. Let's try to obtain - * all columns and than check which columns are present. - */ - // final String[] projection = new String[] { MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.SIZE, MediaStore.MediaColumns.DATA }; - return new CursorLoader(this, uri, null /* all columns, instead of projection */, null, null, null); - } - - @Override - public void onLoaderReset(final Loader loader) { - fileNameView.setText(null); - fileTypeView.setText(null); - fileSizeView.setText(null); - filePath = null; - fileStreamUri = null; - statusOk = false; - } - - @Override - public void onLoadFinished(final Loader loader, final Cursor data) { - if (data != null && data.moveToNext()) { - /* - * Here we have to check the column indexes by name as we have requested for all. The order may be different. - */ - final String fileName = data.getString(data.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)/* 0 DISPLAY_NAME */); - final int fileSize = data.getInt(data.getColumnIndex(MediaStore.MediaColumns.SIZE) /* 1 SIZE */); - String filePath = null; - final int dataIndex = data.getColumnIndex(MediaStore.MediaColumns.DATA); - if (dataIndex != -1) - filePath = data.getString(dataIndex /* 2 DATA */); - if (!TextUtils.isEmpty(filePath)) - this.filePath = filePath; - - updateFileInfo(fileName, fileSize, fileType); - } else { - fileNameView.setText(null); - fileTypeView.setText(null); - fileSizeView.setText(null); - filePath = null; - fileStreamUri = null; - fileStatusView.setText(R.string.dfu_file_status_error); - statusOk = false; - } - } - - /** - * Updates the file information on UI - * - * @param fileName file name - * @param fileSize file length - */ - private void updateFileInfo(final String fileName, final long fileSize, final int fileType) { - fileNameView.setText(fileName); - switch (fileType) { - case DfuService.TYPE_AUTO: - fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[0]); - break; - case DfuService.TYPE_SOFT_DEVICE: - fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[1]); - break; - case DfuService.TYPE_BOOTLOADER: - fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[2]); - break; - case DfuService.TYPE_APPLICATION: - fileTypeView.setText(getResources().getStringArray(R.array.dfu_file_type)[3]); - break; - } - fileSizeView.setText(getString(R.string.dfu_file_size_text, fileSize)); - fileScopeView.setText(getString(R.string.not_available)); - final String extension = this.fileType == DfuService.TYPE_AUTO ? "(?i)ZIP" : "(?i)HEX|BIN"; // (?i) = case insensitive - final boolean statusOk = this.statusOk = MimeTypeMap.getFileExtensionFromUrl(fileName).matches(extension); - fileStatusView.setText(statusOk ? R.string.dfu_file_status_ok : R.string.dfu_file_status_invalid); - uploadButton.setEnabled(selectedDevice != null && statusOk); - - // Ask the user for the Init packet file if HEX or BIN files are selected. In case of a ZIP file the Init packets should be included in the ZIP. - if (statusOk) { - if (fileType != DfuService.TYPE_AUTO) { - scope = null; - fileScopeView.setText(getString(R.string.not_available)); - new AlertDialog.Builder(this) - .setTitle(R.string.dfu_file_init_title) - .setMessage(R.string.dfu_file_init_message) - .setNegativeButton(R.string.no, (dialog, which) -> { - initFilePath = null; - initFileStreamUri = null; - }) - .setPositiveButton(R.string.yes, (dialog, which) -> { - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType(DfuService.MIME_TYPE_OCTET_STREAM); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult(intent, SELECT_INIT_FILE_REQ); - }) - .show(); - } else { - new AlertDialog.Builder(this).setTitle(R.string.dfu_file_scope_title).setCancelable(false) - .setSingleChoiceItems(R.array.dfu_file_scope, 0, (dialog, which) -> { - switch (which) { - case 0: - scope = null; - break; - case 1: - scope = DfuServiceInitiator.SCOPE_SYSTEM_COMPONENTS; - break; - case 2: - scope = DfuServiceInitiator.SCOPE_APPLICATION; - break; - } - }).setPositiveButton(R.string.ok, (dialogInterface, i) -> { - int index; - if (scope == null) { - index = 0; - } else if (scope == DfuServiceInitiator.SCOPE_SYSTEM_COMPONENTS) { - index = 1; - } else { - index = 2; - } - fileScopeView.setText(getResources().getStringArray(R.array.dfu_file_scope)[index]); - }).show(); - } - } - } - - /** - * Called when the question mark was pressed - * - * @param view a button that was pressed - */ - public void onSelectFileHelpClicked(final View view) { - new AlertDialog.Builder(this) - .setTitle(R.string.dfu_help_title) - .setMessage(R.string.dfu_help_message) - .setPositiveButton(R.string.ok, null) - .show(); - } - - /** - * Called when Select File was pressed - * - * @param view a button that was pressed - */ - public void onSelectFileClicked(final View view) { - fileTypeTmp = fileType; - int index = 0; - switch (fileType) { - case DfuService.TYPE_AUTO: - index = 0; - break; - case DfuService.TYPE_SOFT_DEVICE: - index = 1; - break; - case DfuService.TYPE_BOOTLOADER: - index = 2; - break; - case DfuService.TYPE_APPLICATION: - index = 3; - break; - } - // Show a dialog with file types - new AlertDialog.Builder(this) - .setTitle(R.string.dfu_file_type_title) - .setSingleChoiceItems(R.array.dfu_file_type, index, (dialog, which) -> { - switch (which) { - case 0: - fileTypeTmp = DfuService.TYPE_AUTO; - break; - case 1: - fileTypeTmp = DfuService.TYPE_SOFT_DEVICE; - break; - case 2: - fileTypeTmp = DfuService.TYPE_BOOTLOADER; - break; - case 3: - fileTypeTmp = DfuService.TYPE_APPLICATION; - break; - } - }) - .setPositiveButton(R.string.ok, (dialog, which) -> openFileChooser()) - .setNeutralButton(R.string.dfu_file_info, (dialog, which) -> { - final ZipInfoFragment fragment = new ZipInfoFragment(); - fragment.show(getSupportFragmentManager(), "help_fragment"); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - } - - private void openFileChooser() { - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType(fileTypeTmp == DfuService.TYPE_AUTO ? DfuService.MIME_TYPE_ZIP : DfuService.MIME_TYPE_OCTET_STREAM); - intent.addCategory(Intent.CATEGORY_OPENABLE); - if (intent.resolveActivity(getPackageManager()) != null) { - // file browser has been found on the device - startActivityForResult(intent, SELECT_FILE_REQ); - } else { - // there is no any file browser app, let's try to download one - final View customView = getLayoutInflater().inflate(R.layout.app_file_browser, null); - final ListView appsList = customView.findViewById(android.R.id.list); - appsList.setAdapter(new FileBrowserAppsAdapter(this)); - appsList.setChoiceMode(ListView.CHOICE_MODE_SINGLE); - appsList.setItemChecked(0, true); - new AlertDialog.Builder(this) - .setTitle(R.string.dfu_alert_no_filebrowser_title) - .setView(customView) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss()) - .setPositiveButton(R.string.ok, (dialog, which) -> { - final int pos = appsList.getCheckedItemPosition(); - if (pos >= 0) { - final String query = getResources().getStringArray(R.array.dfu_app_file_browser_action)[pos]; - final Intent storeIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(query)); - startActivity(storeIntent); - } - }) - .show(); - } - } - - /** - * Callback of UPDATE/CANCEL button on DfuActivity - */ - public void onUploadClicked(final View view) { - if (isDfuServiceRunning()) { - showUploadCancelDialog(); - return; - } - - // Check whether the selected file is a HEX file (we are just checking the extension) - if (!statusOk) { - Toast.makeText(this, R.string.dfu_file_status_invalid_message, Toast.LENGTH_LONG).show(); - return; - } - - // Save current state in order to restore it if user quit the Activity - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final SharedPreferences.Editor editor = preferences.edit(); - editor.putString(PREFS_DEVICE_NAME, selectedDevice.getName()); - editor.putString(PREFS_FILE_NAME, fileNameView.getText().toString()); - editor.putString(PREFS_FILE_TYPE, fileTypeView.getText().toString()); - editor.putString(PREFS_FILE_SCOPE, fileScopeView.getText().toString()); - editor.putString(PREFS_FILE_SIZE, fileSizeView.getText().toString()); - editor.apply(); - - showProgressBar(); - - final boolean keepBond = preferences.getBoolean(SettingsFragment.SETTINGS_KEEP_BOND, false); - final boolean forceDfu = preferences.getBoolean(SettingsFragment.SETTINGS_ASSUME_DFU_NODE, false); - final boolean enablePRNs = preferences.getBoolean(SettingsFragment.SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, Build.VERSION.SDK_INT < Build.VERSION_CODES.M); - String value = preferences.getString(SettingsFragment.SETTINGS_NUMBER_OF_PACKETS, String.valueOf(DfuServiceInitiator.DEFAULT_PRN_VALUE)); - int numberOfPackets; - try { - numberOfPackets = Integer.parseInt(value); - } catch (final NumberFormatException e) { - numberOfPackets = DfuServiceInitiator.DEFAULT_PRN_VALUE; - } - - final DfuServiceInitiator starter = new DfuServiceInitiator(selectedDevice.getAddress()) - .setDeviceName(selectedDevice.getName()) - .setKeepBond(keepBond) - .setForceDfu(forceDfu) - .setPacketsReceiptNotificationsEnabled(enablePRNs) - .setPacketsReceiptNotificationsValue(numberOfPackets) - .setPrepareDataObjectDelay(400) - .setUnsafeExperimentalButtonlessServiceInSecureDfuEnabled(true); - if (fileType == DfuService.TYPE_AUTO) { - starter.setZip(fileStreamUri, filePath); - if (scope != null) - starter.setScope(scope); - } else { - starter.setBinOrHex(fileType, fileStreamUri, filePath).setInitFile(initFileStreamUri, initFilePath); - } - starter.start(this, DfuService.class); - } - - private void showUploadCancelDialog() { - final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this); - final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION); - pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_PAUSE); - manager.sendBroadcast(pauseAction); - - final UploadCancelFragment fragment = UploadCancelFragment.getInstance(); - fragment.show(getSupportFragmentManager(), TAG); - } - - /** - * Callback of CONNECT/DISCONNECT button on DfuActivity - */ - public void onConnectClicked(final View view) { - if (isBLEEnabled()) { - showDeviceScanningDialog(); - } else { - showBLEDialog(); - } - } - - @Override - public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) { - selectedDevice = device; - uploadButton.setEnabled(statusOk); - deviceNameView.setText(name != null ? name : getString(R.string.not_available)); - } - - @Override - public void onDialogCanceled() { - // do nothing - } - - private void showProgressBar() { - progressBar.setVisibility(View.VISIBLE); - textPercentage.setVisibility(View.VISIBLE); - textPercentage.setText(null); - textUploading.setText(R.string.dfu_status_uploading); - textUploading.setVisibility(View.VISIBLE); - connectButton.setEnabled(false); - selectFileButton.setEnabled(false); - uploadButton.setEnabled(true); - uploadButton.setText(R.string.dfu_action_upload_cancel); - } - - private void onTransferCompleted() { - clearUI(true); - showToast(R.string.dfu_success); - } - - public void onUploadCanceled() { - clearUI(false); - showToast(R.string.dfu_aborted); - } - - @Override - public void onCancelUpload() { - progressBar.setIndeterminate(true); - textUploading.setText(R.string.dfu_status_aborting); - textPercentage.setText(null); - } - - private void showErrorMessage(final String message) { - clearUI(false); - showToast("Upload failed: " + message); - } - - private void clearUI(final boolean clearDevice) { - progressBar.setVisibility(View.INVISIBLE); - textPercentage.setVisibility(View.INVISIBLE); - textUploading.setVisibility(View.INVISIBLE); - connectButton.setEnabled(true); - selectFileButton.setEnabled(true); - uploadButton.setEnabled(false); - uploadButton.setText(R.string.dfu_action_upload); - if (clearDevice) { - selectedDevice = null; - deviceNameView.setText(R.string.dfu_default_name); - } - // Application may have lost the right to these files if Activity was closed during upload (grant uri permission). Clear file related values. - fileNameView.setText(null); - fileTypeView.setText(null); - fileScopeView.setText(null); - fileSizeView.setText(null); - fileStatusView.setText(R.string.dfu_file_status_no_file); - filePath = null; - fileStreamUri = null; - initFilePath = null; - initFileStreamUri = null; - statusOk = false; - } - - private void showToast(final int messageResId) { - Toast.makeText(this, messageResId, Toast.LENGTH_SHORT).show(); - } - - private void showToast(final String message) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - - private boolean isDfuServiceRunning() { - final ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); - for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if (DfuService.class.getName().equals(service.service.getClassName())) { - return true; - } - } - return false; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuInitiatorActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuInitiatorActivity.java deleted file mode 100644 index 341d5fd6..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuInitiatorActivity.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu; - -import android.bluetooth.BluetoothDevice; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; - -/** - * The activity is started only by a remote connected computer using ADB. It shows a list of DFU-supported devices in range and allows user to select target device. The HEX file will be uploaded to - * selected device using {@link DfuService}. - */ -public class DfuInitiatorActivity extends AppCompatActivity implements ScannerFragment.OnDeviceSelectedListener { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // The activity must be started with a path to the HEX file - final Intent intent = getIntent(); - if (!intent.hasExtra(DfuService.EXTRA_FILE_PATH)) - finish(); - - if (savedInstanceState == null) { - final ScannerFragment fragment = ScannerFragment.getInstance(null); // Device that is advertising directly does not have the GENERAL_DISCOVERABLE nor LIMITED_DISCOVERABLE flag set. - fragment.show(getSupportFragmentManager(), null); - } - } - - @Override - public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) { - final Intent intent = getIntent(); - final String overwrittenName = intent.getStringExtra(DfuService.EXTRA_DEVICE_NAME); - final String path = intent.getStringExtra(DfuService.EXTRA_FILE_PATH); - final String initPath = intent.getStringExtra(DfuService.EXTRA_INIT_FILE_PATH); - final String address = device.getAddress(); - final String finalName = overwrittenName == null ? (name != null ? name : getString(R.string.not_available)) : overwrittenName; - final int type = intent.getIntExtra(DfuService.EXTRA_FILE_TYPE, DfuService.TYPE_AUTO); - final boolean keepBond = intent.getBooleanExtra(DfuService.EXTRA_KEEP_BOND, false); - - // Start DFU service with data provided in the intent - final Intent service = new Intent(this, DfuService.class); - service.putExtra(DfuService.EXTRA_DEVICE_ADDRESS, address); - service.putExtra(DfuService.EXTRA_DEVICE_NAME, finalName); - service.putExtra(DfuService.EXTRA_FILE_TYPE, type); - service.putExtra(DfuService.EXTRA_FILE_PATH, path); - if (intent.hasExtra(DfuService.EXTRA_INIT_FILE_PATH)) - service.putExtra(DfuService.EXTRA_INIT_FILE_PATH, initPath); - service.putExtra(DfuService.EXTRA_KEEP_BOND, keepBond); - service.putExtra(DfuService.EXTRA_UNSAFE_EXPERIMENTAL_BUTTONLESS_DFU, true); - startService(service); - finish(); - } - - @Override - public void onDialogCanceled() { - finish(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuService.java deleted file mode 100644 index 2b16b597..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/DfuService.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu; - -import android.app.Activity; - -import no.nordicsemi.android.dfu.DfuBaseService; - -public class DfuService extends DfuBaseService { - - @Override - protected Class getNotificationTarget() { - /* - * As a target activity the NotificationActivity is returned, not the MainActivity. This is because the notification must create a new task: - * - * intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - * - * when user press it. Using NotificationActivity we can check whether the new activity is a root activity (that means no other activity was open before) - * or that there is other activity already open. In the later case the notificationActivity will just be closed. System will restore the previous activity. - * However if the application has been closed during upload and user click the notification a NotificationActivity will be launched as a root activity. - * It will create and start the main activity and terminate itself. - * - * This method may be used to restore the target activity in case the application was closed or is open. It may also be used to recreate an activity - * history (see NotificationActivity). - */ - return NotificationActivity.class; - } - - @Override - protected boolean isDebug() { - // return BuildConfig.DEBUG; - return true; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/NotificationActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/NotificationActivity.java deleted file mode 100644 index fd07aec2..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/NotificationActivity.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; - -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; - -public class NotificationActivity extends Activity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // If this activity is the root activity of the task, the app is not running - if (isTaskRoot()) { - // Start the app before finishing - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent startAppIntent = new Intent(this, DfuActivity.class); - if (getIntent() != null && getIntent().getExtras() != null) - startAppIntent.putExtras(getIntent().getExtras()); - startActivities(new Intent[] { parentIntent, startAppIntent }); - } - - // Now finish, which will drop the user in to the activity that was at the top - // of the task stack - finish(); - } -} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/adapter/FileBrowserAppsAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/adapter/FileBrowserAppsAdapter.java deleted file mode 100644 index 64ed30c2..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/adapter/FileBrowserAppsAdapter.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu.adapter; - -import android.content.Context; -import android.content.res.Resources; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import no.nordicsemi.android.nrftoolbox.R; - -/** - * This adapter displays some file browser applications that can be used to select HEX file. It is used when there is no such app already installed on the device. The hardcoded apps and Google Play - * URLs are specified in res/values/strings_dfu.xml. - */ -public class FileBrowserAppsAdapter extends BaseAdapter { - private final LayoutInflater inflater; - private final Resources resources; - - public FileBrowserAppsAdapter(final Context context) { - inflater = LayoutInflater.from(context); - resources = context.getResources(); - } - - @Override - public int getCount() { - return resources.getStringArray(R.array.dfu_app_file_browser).length; - } - - @Override - public Object getItem(final int position) { - return resources.getStringArray(R.array.dfu_app_file_browser_action)[position]; - } - - @Override - public long getItemId(final int position) { - return position; - } - - @Override - public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) { - View view = convertView; - if (view == null) { - view = inflater.inflate(R.layout.app_file_browser_item, parent, false); - } - - final TextView item = (TextView) view; - item.setText(resources.getStringArray(R.array.dfu_app_file_browser)[position]); - item.getCompoundDrawablesRelative()[0].setLevel(position); - return view; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/UploadCancelFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/UploadCancelFragment.java deleted file mode 100644 index 0eceff82..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/UploadCancelFragment.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu.fragment; - -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.dfu.DfuService; - -/** - * When cancel button is pressed during uploading this fragment shows uploading cancel dialog - */ -public class UploadCancelFragment extends DialogFragment { - private static final String TAG = "UploadCancelFragment"; - - private CancelFragmentListener listener; - - public interface CancelFragmentListener { - void onCancelUpload(); - } - - public static UploadCancelFragment getInstance() { - return new UploadCancelFragment(); - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - - try { - listener = (CancelFragmentListener) context; - } catch (final ClassCastException e) { - Log.d(TAG, "The parent Activity must implement CancelFragmentListener interface"); - } - } - - @NonNull - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - return new AlertDialog.Builder(requireContext()).setTitle(R.string.dfu_confirmation_dialog_title).setMessage(R.string.dfu_upload_dialog_cancel_message).setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, whichButton) -> { - final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(requireContext()); - final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION); - pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_ABORT); - manager.sendBroadcast(pauseAction); - - listener.onCancelUpload(); - }).setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()).create(); - } - - @Override - public void onCancel(@NonNull final DialogInterface dialog) { - final LocalBroadcastManager manager = LocalBroadcastManager.getInstance(requireContext()); - final Intent pauseAction = new Intent(DfuService.BROADCAST_ACTION); - pauseAction.putExtra(DfuService.EXTRA_ACTION, DfuService.ACTION_RESUME); - manager.sendBroadcast(pauseAction); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/ZipInfoFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/ZipInfoFragment.java deleted file mode 100644 index e8519ab2..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/fragment/ZipInfoFragment.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu.fragment; - -import android.app.Dialog; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.appcompat.app.AlertDialog; -import android.view.LayoutInflater; -import android.view.View; - -import no.nordicsemi.android.nrftoolbox.R; - -public class ZipInfoFragment extends DialogFragment { - - @Override - @NonNull - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final View view = LayoutInflater.from(requireContext()) - .inflate(R.layout.fragment_zip_info, null); - return new AlertDialog.Builder(requireContext()) - .setView(view) - .setTitle(R.string.dfu_file_info) - .setPositiveButton(R.string.ok, null) - .create(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/AboutDfuPreference.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/AboutDfuPreference.java deleted file mode 100644 index 4b751a7d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/AboutDfuPreference.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu.settings; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.util.AttributeSet; -import android.widget.Toast; - -import androidx.preference.Preference; -import no.nordicsemi.android.nrftoolbox.R; - -public class AboutDfuPreference extends Preference { - - public AboutDfuPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public AboutDfuPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - @Override - protected void onClick() { - final Context context = getContext(); - final Intent intent = new Intent(Intent.ACTION_VIEW, - Uri.parse("https://infocenter.nordicsemi.com/topic/sdk_nrf5_v16.0.0/examples_bootloader.html?cp=7_1_4_4")); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - // is browser installed? - if (intent.resolveActivity(context.getPackageManager()) != null) - context.startActivity(intent); - else { - Toast.makeText(getContext(), R.string.no_application, Toast.LENGTH_LONG).show(); - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsActivity.java deleted file mode 100644 index a957f339..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsActivity.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu.settings; - -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.MenuItem; - -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsActivity extends AppCompatActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Display the fragment as the main content. - getSupportFragmentManager().beginTransaction().replace(R.id.content, new SettingsFragment()).commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsFragment.java deleted file mode 100644 index aaaf5de3..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/dfu/settings/SettingsFragment.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) 2015, 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.dfu.settings; - -import android.content.SharedPreferences; -import android.os.Build; -import android.os.Bundle; -import androidx.appcompat.app.AlertDialog; -import android.text.TextUtils; - -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceScreen; -import no.nordicsemi.android.dfu.DfuServiceInitiator; -import no.nordicsemi.android.dfu.DfuSettingsConstants; -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsFragment extends PreferenceFragmentCompat implements DfuSettingsConstants, SharedPreferences.OnSharedPreferenceChangeListener { - public static final String SETTINGS_KEEP_BOND = "settings_keep_bond"; - - @Override - public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.settings_dfu); - - // set initial values - updateNumberOfPacketsSummary(); - updateMBRSize(); - } - - @Override - public void onResume() { - super.onResume(); - - // attach the preference change listener. It will update the summary below interval preference - getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); - } - - @Override - public void onPause() { - super.onPause(); - - // unregister listener - getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); - } - - @Override - public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) { - final SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); - - if (SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED.equals(key)) { - final boolean disabled = !preferences.getBoolean(SETTINGS_PACKET_RECEIPT_NOTIFICATION_ENABLED, true); - if (disabled && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - new AlertDialog.Builder(requireContext()).setMessage(R.string.dfu_settings_dfu_number_of_packets_info).setTitle(R.string.dfu_settings_dfu_information) - .setPositiveButton(R.string.ok, null).show(); - } - } else if (SETTINGS_NUMBER_OF_PACKETS.equals(key)) { - updateNumberOfPacketsSummary(); - } else if (SETTINGS_MBR_SIZE.equals(key)) { - updateMBRSize(); - } else if (SETTINGS_ASSUME_DFU_NODE.equals(key) && sharedPreferences.getBoolean(key, false)) { - new AlertDialog.Builder(requireContext()).setMessage(R.string.dfu_settings_dfu_assume_dfu_mode_info).setTitle(R.string.dfu_settings_dfu_information) - .setPositiveButton(R.string.ok, null) - .show(); - } - } - - private void updateNumberOfPacketsSummary() { - final PreferenceScreen screen = getPreferenceScreen(); - final SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); - - String value = preferences.getString(SETTINGS_NUMBER_OF_PACKETS, String.valueOf(SETTINGS_NUMBER_OF_PACKETS_DEFAULT)); - // Security check - if (TextUtils.isEmpty(value)) { - value = String.valueOf(SETTINGS_NUMBER_OF_PACKETS_DEFAULT); - preferences.edit().putString(SETTINGS_NUMBER_OF_PACKETS, value).apply(); - } - screen.findPreference(SETTINGS_NUMBER_OF_PACKETS).setSummary(value); - - final int valueInt = Integer.parseInt(value); - if (valueInt > 200 && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - new AlertDialog.Builder(requireContext()).setMessage(R.string.dfu_settings_dfu_number_of_packets_info).setTitle(R.string.dfu_settings_dfu_information) - .setPositiveButton(R.string.ok, null) - .show(); - } - } - - private void updateMBRSize() { - final PreferenceScreen screen = getPreferenceScreen(); - final SharedPreferences preferences = getPreferenceManager().getSharedPreferences(); - - final String value = preferences.getString(SETTINGS_MBR_SIZE, String.valueOf(DfuServiceInitiator.DEFAULT_MBR_SIZE)); - screen.findPreference(SETTINGS_MBR_SIZE).setSummary(value); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/ExpandableRecordAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/ExpandableRecordAdapter.java deleted file mode 100644 index 6bc6cbad..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/ExpandableRecordAdapter.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright (c) 2015, 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.gls; - -import android.content.Context; -import android.content.res.Resources; -import android.util.Pair; -import android.util.SparseArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import android.widget.TextView; - -import no.nordicsemi.android.nrftoolbox.R; - -public class ExpandableRecordAdapter extends BaseExpandableListAdapter {; - private final GlucoseManager glucoseManager; - private final LayoutInflater inflater; - private final Context context; - private SparseArray records; - - ExpandableRecordAdapter(final Context context, final GlucoseManager manager) { - this.glucoseManager = manager; - this.context = context; - inflater = LayoutInflater.from(context); - records = manager.getRecords().clone(); - } - - @Override - public void notifyDataSetChanged() { - records = glucoseManager.getRecords().clone(); - super.notifyDataSetChanged(); - } - - @Override - public int getGroupCount() { - return records.size(); - } - - @Override - public Object getGroup(final int groupPosition) { - return records.valueAt(groupPosition); - } - - @Override - public long getGroupId(final int groupPosition) { - return records.keyAt(groupPosition); - } - - @Override - public View getGroupView(final int position, boolean isExpanded, final View convertView, final ViewGroup parent) { - View view = convertView; - if (view == null) { - view = inflater.inflate(R.layout.activity_feature_gls_item, parent, false); - - final GroupViewHolder holder = new GroupViewHolder(); - holder.time = view.findViewById(R.id.time); - holder.details = view.findViewById(R.id.details); - holder.concentration = view.findViewById(R.id.gls_concentration); - view.setTag(holder); - } - final GlucoseRecord record = (GlucoseRecord) getGroup(position); - if (record == null) - return view; // this may happen during closing the activity - final GroupViewHolder holder = (GroupViewHolder) view.getTag(); - holder.time.setText(context.getString(R.string.gls_timestamp, record.time)); - try { - holder.details.setText(context.getResources().getStringArray(R.array.gls_type)[record.type]); - } catch (final ArrayIndexOutOfBoundsException e) { - holder.details.setText(context.getResources().getStringArray(R.array.gls_type)[0]); - } - if (record.unit == GlucoseRecord.UNIT_kgpl) { - holder.concentration.setText(context.getString(R.string.gls_value, record.glucoseConcentration * 100000.0f)); - } else { - holder.concentration.setText(context.getString(R.string.gls_value, record.glucoseConcentration * 1000.0f)); - } - return view; - } - - @Override - public int getChildrenCount(final int groupPosition) { - final GlucoseRecord record = (GlucoseRecord) getGroup(groupPosition); - int count = 1 + (record.status != 0 ? 1 : 0); // Sample Location and optional Sensor Status Annunciation - if (record.context != null) { - final GlucoseRecord.MeasurementContext context = record.context; - if (context.carbohydrateId != 0) - count += 1; // Carbohydrate ID and units - if (context.meal != 0) - count += 1; // Meal - if (context.tester != 0) - count += 1; // Tester - if (context.health != 0) - count += 1; // Health - if (context.exerciseDuration != 0) - count += 1; // Duration and intensity - if (context.medicationId != 0) - count += 1; // Medication ID and quantity (with unit) - if (context.HbA1c != 0) - count += 1; // HbA1c - } - return count; - } - - @Override - public Object getChild(final int groupPosition, final int childPosition) { - final Resources resources = context.getResources(); - final GlucoseRecord record = (GlucoseRecord) getGroup(groupPosition); - String tmp; - switch (childIdToItemId(childPosition, record)) { - case 0: - try { - tmp = resources.getStringArray(R.array.gls_location)[record.sampleLocation]; - } catch (final ArrayIndexOutOfBoundsException e) { - tmp = resources.getStringArray(R.array.gls_location)[0]; - } - return new Pair<>(resources.getString(R.string.gls_location_title), tmp); - case 1: { // sensor status annunciation - final StringBuilder builder = new StringBuilder(); - final int status = record.status; - for (int i = 0; i < 12; ++i) - if ((status & (1 << i)) > 0) - builder.append(resources.getStringArray(R.array.gls_status_annunciation)[i]).append("\n"); - builder.setLength(builder.length() - 1); - return new Pair<>(resources.getString(R.string.gls_status_annunciation_title), builder.toString()); - } - case 2: { // carbohydrate id and unit - try { - tmp = resources.getStringArray(R.array.gls_context_carbohydrare)[record.context.carbohydrateId]; - } catch (final ArrayIndexOutOfBoundsException e) { - tmp = resources.getStringArray(R.array.gls_context_carbohydrare)[0]; - } - return new Pair<>(resources.getString(R.string.gls_context_carbohydrare_title), tmp + " (" + record.context.carbohydrateUnits + " g)"); - } - case 3: { // meal - try { - tmp = resources.getStringArray(R.array.gls_context_meal)[record.context.meal]; - } catch (final ArrayIndexOutOfBoundsException e) { - tmp = resources.getStringArray(R.array.gls_context_meal)[0]; - } - return new Pair<>(resources.getString(R.string.gls_context_meal_title), tmp); - } - case 4: { // tester - try { - tmp = resources.getStringArray(R.array.gls_context_tester)[record.context.tester]; - } catch (final ArrayIndexOutOfBoundsException e) { - tmp = resources.getStringArray(R.array.gls_context_tester)[0]; - } - return new Pair<>(resources.getString(R.string.gls_context_tester_title), tmp); - } - case 5: { // health - try { - tmp = resources.getStringArray(R.array.gls_context_health)[record.context.health]; - } catch (final ArrayIndexOutOfBoundsException e) { - tmp = resources.getStringArray(R.array.gls_context_health)[0]; - } - return new Pair<>(resources.getString(R.string.gls_context_health_title), tmp); - } - case 6: { // exercise duration and intensity - return new Pair<>(resources.getString(R.string.gls_context_exercise_title), resources.getString(R.string.gls_context_exercise, record.context.exerciseDuration, record.context.exerciseIntensity)); - } - case 7: { // medication ID and quantity - try { - tmp = resources.getStringArray(R.array.gls_context_medication_id)[record.context.medicationId]; - } catch (final ArrayIndexOutOfBoundsException e) { - tmp = resources.getStringArray(R.array.gls_context_medication_id)[0]; - } - final int resId = record.context.medicationUnit == GlucoseRecord.UNIT_kgpl ? R.string.gls_context_medication_kg : R.string.gls_context_medication_l; - return new Pair<>(resources.getString(R.string.gls_context_medication_title), resources.getString(resId, tmp, record.context.medicationQuantity)); - } - case 8: { // HbA1c value - return new Pair<>(resources.getString(R.string.gls_context_hba1c_title), resources.getString(R.string.gls_context_hba1c, record.context.HbA1c)); - } - default: - return new Pair<>("Not implemented", "The value exists but is not shown"); - } - } - - private int childIdToItemId(final int childPosition, final GlucoseRecord record) { - int itemId = 0; - int child = childPosition; - - // Location is required - if (itemId == childPosition) - return itemId; - - if (++itemId > 0 && record.status != 0 && --child == 0) return itemId; - if (record.context != null) { - if (++itemId > 0 && record.context.carbohydrateId != 0 && --child == 0) return itemId; - if (++itemId > 0 && record.context.meal != 0 && --child == 0) return itemId; - if (++itemId > 0 && record.context.tester != 0 && --child == 0) return itemId; - if (++itemId > 0 && record.context.health != 0 && --child == 0) return itemId; - if (++itemId > 0 && record.context.exerciseDuration != 0 && --child == 0) return itemId; - if (++itemId > 0 && record.context.medicationId != 0 && --child == 0) return itemId; - if (++itemId > 0 && record.context.HbA1c != 0 && --child == 0) return itemId; - } - throw new IllegalArgumentException("No item ID for position " + childPosition); - } - - @Override - public long getChildId(final int groupPosition, final int childPosition) { - return groupPosition + childPosition; - } - - @SuppressWarnings("unchecked") - @Override - public View getChildView(final int groupPosition, final int childPosition, final boolean isLastChild, final View convertView, final ViewGroup parent) { - View view = convertView; - if (view == null) { - view = inflater.inflate(R.layout.activity_feature_gls_subitem, parent, false); - final ChildViewHolder holder = new ChildViewHolder(); - holder.title = view.findViewById(android.R.id.text1); - holder.details = view.findViewById(android.R.id.text2); - view.setTag(holder); - } - final Pair value = (Pair) getChild(groupPosition, childPosition); - final ChildViewHolder holder = (ChildViewHolder) view.getTag(); - holder.title.setText(value.first); - holder.details.setText(value.second); - return view; - } - - @Override - public boolean hasStableIds() { - return true; - } - - @Override - public boolean isChildSelectable(final int groupPosition, final int childPosition) { - return false; - } - - private class GroupViewHolder { - private TextView time; - private TextView details; - private TextView concentration; - } - - private class ChildViewHolder { - private TextView title; - private TextView details; - } - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseActivity.java deleted file mode 100644 index f9d90c3f..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseActivity.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (c) 2015, 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.gls; - -import android.bluetooth.BluetoothDevice; -import android.os.Bundle; -import androidx.annotation.NonNull; -import android.util.SparseArray; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.BaseExpandableListAdapter; -import android.widget.PopupMenu; -import android.widget.TextView; - -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileExpandableListActivity; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -// TODO The GlucoseActivity should be rewritten to use the service approach, like other do. -public class GlucoseActivity extends BleProfileExpandableListActivity implements PopupMenu.OnMenuItemClickListener, GlucoseManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "GlucoseActivity"; - - private BaseExpandableListAdapter adapter; - private GlucoseManager glucoseManager; - - private View controlPanelStd; - private View controlPanelAbort; - private TextView unitView; - private TextView batteryLevelView; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_gls); - setGUI(); - } - - private void setGUI() { - unitView = findViewById(R.id.unit); - controlPanelStd = findViewById(R.id.gls_control_std); - controlPanelAbort = findViewById(R.id.gls_control_abort); - batteryLevelView = findViewById(R.id.battery); - - findViewById(R.id.action_last).setOnClickListener(v -> glucoseManager.getLastRecord()); - findViewById(R.id.action_all).setOnClickListener(v -> glucoseManager.getAllRecords()); - findViewById(R.id.action_abort).setOnClickListener(v -> glucoseManager.abort()); - - // create popup menu attached to the button More - findViewById(R.id.action_more).setOnClickListener(v -> { - PopupMenu menu = new PopupMenu(GlucoseActivity.this, v); - menu.setOnMenuItemClickListener(GlucoseActivity.this); - MenuInflater inflater = menu.getMenuInflater(); - inflater.inflate(R.menu.gls_more, menu.getMenu()); - menu.show(); - }); - - setListAdapter(adapter = new ExpandableRecordAdapter(this, glucoseManager)); - } - - @Override - protected LoggableBleManager initializeManager() { - final GlucoseManager manager = glucoseManager = GlucoseManager.getGlucoseManager(getApplicationContext()); - manager.setGattCallbacks(this); - return manager; - } - - @Override - public boolean onMenuItemClick(final MenuItem item) { - switch (item.getItemId()) { - case R.id.action_refresh: - glucoseManager.refreshRecords(); - break; - case R.id.action_first: - glucoseManager.getFirstRecord(); - break; - case R.id.action_clear: - glucoseManager.clear(); - break; - case R.id.action_delete_all: - glucoseManager.deleteAllRecords(); - break; - } - return true; - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.gls_feature_title; - } - - @Override - protected int getAboutTextId() { - return R.string.gls_about_text; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.gls_default_name; - } - - @Override - protected UUID getFilterUUID() { - return GlucoseManager.GLS_SERVICE_UUID; - } - - @Override - protected void setDefaultUI() { - glucoseManager.clear(); - batteryLevelView.setText(R.string.not_available); - } - - private void setOperationInProgress(final boolean progress) { - runOnUiThread(() -> { - controlPanelStd.setVisibility(!progress ? View.VISIBLE : View.GONE); - controlPanelAbort.setVisibility(progress ? View.VISIBLE : View.GONE); - }); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - setOperationInProgress(false); - runOnUiThread(() -> batteryLevelView.setText(R.string.not_available)); - } - - @Override - public void onOperationStarted(@NonNull final BluetoothDevice device) { - setOperationInProgress(true); - } - - @Override - public void onOperationCompleted(@NonNull final BluetoothDevice device) { - setOperationInProgress(false); - - runOnUiThread(() -> { - final SparseArray records = glucoseManager.getRecords(); - if (records.size() > 0) { - final int unit = records.valueAt(0).unit; - unitView.setVisibility(View.VISIBLE); - unitView.setText(unit == GlucoseRecord.UNIT_kgpl ? R.string.gls_unit_mgpdl : R.string.gls_unit_mmolpl); - } else { - unitView.setVisibility(View.GONE); - } - adapter.notifyDataSetChanged(); - }); - } - - @Override - public void onOperationAborted(@NonNull final BluetoothDevice device) { - setOperationInProgress(false); - } - - @Override - public void onOperationNotSupported(@NonNull final BluetoothDevice device) { - setOperationInProgress(false); - showToast(R.string.gls_operation_not_supported); - } - - @Override - public void onOperationFailed(@NonNull final BluetoothDevice device) { - setOperationInProgress(false); - showToast(R.string.gls_operation_failed); - } - - @Override - public void onDataSetChanged(@NonNull final BluetoothDevice device) { - // Do nothing. Refreshing the list is done in onOperationCompleted - } - - @Override - public void onNumberOfRecordsRequested(@NonNull final BluetoothDevice device, final int value) { - if (value == 0) - showToast(R.string.gls_progress_zero); - else - showToast(getResources().getQuantityString(R.plurals.gls_progress, value, value)); - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - runOnUiThread(() -> batteryLevelView.setText(getString(R.string.battery, batteryLevel))); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManager.java deleted file mode 100644 index 66cf19cd..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManager.java +++ /dev/null @@ -1,434 +0,0 @@ -/* - * Copyright (c) 2015, 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.gls; - -import android.annotation.SuppressLint; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import android.os.Handler; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.util.Log; -import android.util.SparseArray; - -import java.util.Calendar; -import java.util.UUID; - -import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback; -import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback; -import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback; -import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.GlucoseMeasurementContextParser; -import no.nordicsemi.android.nrftoolbox.parser.GlucoseMeasurementParser; -import no.nordicsemi.android.nrftoolbox.parser.RecordAccessControlPointParser; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; - -@SuppressWarnings("unused") -public class GlucoseManager extends BatteryManager { - private static final String TAG = "GlucoseManager"; - - /** Glucose service UUID */ - final static UUID GLS_SERVICE_UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb"); - /** Glucose Measurement characteristic UUID */ - private final static UUID GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb"); - /** Glucose Measurement Context characteristic UUID */ - private final static UUID GM_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb"); - /** Glucose Feature characteristic UUID */ - private final static UUID GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb"); - /** Record Access Control Point characteristic UUID */ - private final static UUID RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb"); - - private BluetoothGattCharacteristic glucoseMeasurementCharacteristic; - private BluetoothGattCharacteristic glucoseMeasurementContextCharacteristic; - private BluetoothGattCharacteristic recordAccessControlPointCharacteristic; - - private final SparseArray records = new SparseArray<>(); - private Handler handler; - private static GlucoseManager instance; - - /** - * Returns the singleton implementation of GlucoseManager. - */ - static GlucoseManager getGlucoseManager(@NonNull final Context context) { - if (instance == null) - instance = new GlucoseManager(context); - return instance; - } - - private GlucoseManager(final Context context) { - super(context); - handler = new Handler(); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new GlucoseManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving notification, etc. - */ - private class GlucoseManagerGattCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - super.initialize(); - - // The gatt.setCharacteristicNotification(...) method is called in BleManager during - // enabling notifications or indications - // (see BleManager#internalEnableNotifications/Indications). - // However, on Samsung S3 with Android 4.3 it looks like the 2 gatt calls - // (gatt.setCharacteristicNotification(...) and gatt.writeDescriptor(...)) are called - // too quickly, or from a wrong thread, and in result the notification listener is not - // set, causing onCharacteristicChanged(...) callback never being called when a - // notification comes. Enabling them here, like below, solves the problem. - // However... the original approach works for the Battery Level CCCD, which makes it - // even weirder. - /* - gatt.setCharacteristicNotification(glucoseMeasurementCharacteristic, true); - if (glucoseMeasurementContextCharacteristic != null) { - device.setCharacteristicNotification(glucoseMeasurementContextCharacteristic, true); - } - device.setCharacteristicNotification(recordAccessControlPointCharacteristic, true); - */ - setNotificationCallback(glucoseMeasurementCharacteristic) - .with(new GlucoseMeasurementDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + GlucoseMeasurementParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @Override - public void onGlucoseMeasurementReceived(@NonNull final BluetoothDevice device, final int sequenceNumber, - @NonNull final Calendar time, @Nullable final Float glucoseConcentration, - @Nullable final Integer unit, @Nullable final Integer type, - @Nullable final Integer sampleLocation, @Nullable final GlucoseStatus status, - final boolean contextInformationFollows) { - final GlucoseRecord record = new GlucoseRecord(); - record.sequenceNumber = sequenceNumber; - record.time = time; - record.glucoseConcentration = glucoseConcentration != null ? glucoseConcentration : 0; - record.unit = unit != null ? unit : UNIT_kg_L; - record.type = type != null ? type : 0; - record.sampleLocation = sampleLocation != null ? sampleLocation : 0; - record.status = status != null ? status.value : 0; - - // insert the new record to storage - records.put(record.sequenceNumber, record); - handler.post(() -> { - // if there is no context information following the measurement data, - // notify callback about the new record - if (!contextInformationFollows) - mCallbacks.onDataSetChanged(device); - }); - } - }); - - setNotificationCallback(glucoseMeasurementContextCharacteristic) - .with(new GlucoseMeasurementContextDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + GlucoseMeasurementContextParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @Override - public void onGlucoseMeasurementContextReceived(@NonNull final BluetoothDevice device, final int sequenceNumber, - @Nullable final Carbohydrate carbohydrate, @Nullable final Float carbohydrateAmount, - @Nullable final Meal meal, @Nullable final Tester tester, - @Nullable final Health health, @Nullable final Integer exerciseDuration, - @Nullable final Integer exerciseIntensity, @Nullable final Medication medication, - @Nullable final Float medicationAmount, @Nullable final Integer medicationUnit, - @Nullable final Float HbA1c) { - final GlucoseRecord record = records.get(sequenceNumber); - if (record == null) { - DebugLogger.w(TAG, "Context information with unknown sequence number: " + sequenceNumber); - return; - } - final GlucoseRecord.MeasurementContext context = new GlucoseRecord.MeasurementContext(); - record.context = context; - context.carbohydrateId = carbohydrate != null ? carbohydrate.value : 0; - context.carbohydrateUnits = carbohydrateAmount != null ? carbohydrateAmount : 0; - context.meal = meal != null ? meal.value : 0; - context.tester = tester != null ? tester.value : 0; - context.health = health != null ? health.value : 0; - context.exerciseDuration = exerciseDuration != null ? exerciseDuration : 0; - context.exerciseIntensity = exerciseIntensity != null ? exerciseIntensity : 0; - context.medicationId = medication != null ? medication.value : 0; - context.medicationQuantity = medicationAmount != null ? medicationAmount : 0; - context.medicationUnit = medicationUnit != null ? medicationUnit : UNIT_mg; - context.HbA1c = HbA1c != null ? HbA1c : 0; - - handler.post(() -> { - // notify callback about the new record - mCallbacks.onDataSetChanged(device); - }); - } - }); - - setIndicationCallback(recordAccessControlPointCharacteristic) - .with(new RecordAccessControlPointDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @SuppressLint("SwitchIntDef") - @Override - public void onRecordAccessOperationCompleted(@NonNull final BluetoothDevice device, - @RACPOpCode final int requestCode) { - //noinspection SwitchStatementWithTooFewBranches - switch (requestCode) { - case RACP_OP_CODE_ABORT_OPERATION: - mCallbacks.onOperationAborted(device); - break; - default: - mCallbacks.onOperationCompleted(device); - break; - } - } - - @Override - public void onRecordAccessOperationCompletedWithNoRecordsFound(@NonNull final BluetoothDevice device, - @RACPOpCode final int requestCode) { - mCallbacks.onOperationCompleted(device); - } - - @Override - public void onNumberOfRecordsReceived(@NonNull final BluetoothDevice device, final int numberOfRecords) { - mCallbacks.onNumberOfRecordsRequested(device, numberOfRecords); - if (numberOfRecords > 0) { - if (records.size() > 0) { - final int sequenceNumber = records.keyAt(records.size() - 1) + 1; - writeCharacteristic(recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)) - .enqueue(); - } else { - writeCharacteristic(recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportAllStoredRecords()) - .enqueue(); - } - } else { - mCallbacks.onOperationCompleted(device); - } - } - - @Override - public void onRecordAccessOperationError(@NonNull final BluetoothDevice device, - @RACPOpCode final int requestCode, - @RACPErrorCode final int errorCode) { - log(Log.WARN, "Record Access operation failed (error " + errorCode + ")"); - if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - mCallbacks.onOperationNotSupported(device); - } else { - mCallbacks.onOperationFailed(device); - } - } - }); - - enableNotifications(glucoseMeasurementCharacteristic).enqueue(); - enableNotifications(glucoseMeasurementContextCharacteristic).enqueue(); - enableIndications(recordAccessControlPointCharacteristic) - .fail((device, status) -> log(Log.WARN, "Failed to enabled Record Access Control Point indications (error " + status + ")")) - .enqueue(); - } - - @Override - public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(GLS_SERVICE_UUID); - if (service != null) { - glucoseMeasurementCharacteristic = service.getCharacteristic(GM_CHARACTERISTIC); - glucoseMeasurementContextCharacteristic = service.getCharacteristic(GM_CONTEXT_CHARACTERISTIC); - recordAccessControlPointCharacteristic = service.getCharacteristic(RACP_CHARACTERISTIC); - } - return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null; - } - - @Override - protected boolean isOptionalServiceSupported(@NonNull BluetoothGatt gatt) { - super.isOptionalServiceSupported(gatt); - return glucoseMeasurementContextCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - glucoseMeasurementCharacteristic = null; - glucoseMeasurementContextCharacteristic = null; - recordAccessControlPointCharacteristic = null; - } - } - - /** - * Returns all records as a sparse array where sequence number is the key. - * - * @return the records list. - */ - SparseArray getRecords() { - return records; - } - - /** - * Clears the records list locally. - */ - public void clear() { - records.clear(); - final BluetoothDevice target = getBluetoothDevice(); - if (target != null) { - mCallbacks.onOperationCompleted(target); - } - } - - /** - * Sends the request to obtain the last (most recent) record from glucose device. The data will - * be returned to Glucose Measurement characteristic as a notification followed by Record Access - * Control Point indication with status code Success or other in case of error. - */ - void getLastRecord() { - if (recordAccessControlPointCharacteristic == null) - return; - final BluetoothDevice target = getBluetoothDevice(); - if (target == null) - return; - - clear(); - mCallbacks.onOperationStarted(target); - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportLastStoredRecord()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends the request to obtain the first (oldest) record from glucose device. The data will be - * returned to Glucose Measurement characteristic as a notification followed by Record Access - * Control Point indication with status code Success or other in case of error. - */ - void getFirstRecord() { - if (recordAccessControlPointCharacteristic == null) - return; - final BluetoothDevice target = getBluetoothDevice(); - if (target == null) - return; - - clear(); - mCallbacks.onOperationStarted(target); - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportFirstStoredRecord()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends the request to obtain all records from glucose device. Initially we want to notify user - * about the number of the records so the 'Report Number of Stored Records' is send. The data - * will be returned to Glucose Measurement characteristic as a notification followed by - * Record Access Control Point indication with status code Success or other in case of error. - */ - void getAllRecords() { - if (recordAccessControlPointCharacteristic == null) - return; - final BluetoothDevice target = getBluetoothDevice(); - if (target == null) - return; - - clear(); - mCallbacks.onOperationStarted(target); - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportNumberOfAllStoredRecords()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends the request to obtain from the glucose device all records newer than the newest one - * from local storage. The data will be returned to Glucose Measurement characteristic as - * a notification followed by Record Access Control Point indication with status code Success - * or other in case of error. - *

- * Refresh button will not download records older than the oldest in the local memory. - * E.g. if you have pressed Last and then Refresh, than it will try to get only newer records. - * However if there are no records, it will download all existing (using {@link #getAllRecords()}). - */ - void refreshRecords() { - if (recordAccessControlPointCharacteristic == null) - return; - final BluetoothDevice target = getBluetoothDevice(); - if (target == null) - return; - - if (records.size() == 0) { - getAllRecords(); - } else { - mCallbacks.onOperationStarted(target); - - // obtain the last sequence number - final int sequenceNumber = records.keyAt(records.size() - 1) + 1; - - writeCharacteristic(recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - // Info: - // Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2. - } - } - - /** - * Sends abort operation signal to the device. - */ - void abort() { - if (recordAccessControlPointCharacteristic == null) - return; - final BluetoothDevice target = getBluetoothDevice(); - if (target == null) - return; - - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.abortOperation()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } - - /** - * Sends the request to delete all data from the device. A Record Access Control Point - * indication with status code Success (or other in case of error) will be send. - */ - void deleteAllRecords() { - if (recordAccessControlPointCharacteristic == null) - return; - final BluetoothDevice target = getBluetoothDevice(); - if (target == null) - return; - - clear(); - mCallbacks.onOperationStarted(target); - writeCharacteristic(recordAccessControlPointCharacteristic, RecordAccessControlPointData.deleteAllStoredRecords()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, "\"" + RecordAccessControlPointParser.parse(data) + "\" sent")) - .enqueue(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManagerCallbacks.java deleted file mode 100644 index e89c04bb..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseManagerCallbacks.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2015, 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.gls; - -import android.bluetooth.BluetoothDevice; - -import androidx.annotation.NonNull; -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -public interface GlucoseManagerCallbacks extends BatteryManagerCallbacks { - - void onOperationStarted(@NonNull final BluetoothDevice device); - - void onOperationCompleted(@NonNull final BluetoothDevice device); - - void onOperationFailed(@NonNull final BluetoothDevice device); - - void onOperationAborted(@NonNull final BluetoothDevice device); - - void onOperationNotSupported(@NonNull final BluetoothDevice device); - - void onDataSetChanged(@NonNull final BluetoothDevice device); - - void onNumberOfRecordsRequested(@NonNull final BluetoothDevice device, final int value); -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseRecord.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseRecord.java deleted file mode 100644 index f263a0e8..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/gls/GlucoseRecord.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2015, 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.gls; - -import java.util.Calendar; - -@SuppressWarnings("unused") -public class GlucoseRecord { - static final int UNIT_kgpl = 0; - private static final int UNIT_molpl = 1; - - /** Record sequence number */ - int sequenceNumber; - /** The base time of the measurement */ - Calendar time; - /** Time offset of the record */ - int timeOffset; - /** The glucose concentration. 0 if not present */ - float glucoseConcentration; - /** Concentration unit. One of the following: {@link GlucoseRecord#UNIT_kgpl}, {@link GlucoseRecord#UNIT_molpl} */ - int unit; - /** The type of the record. 0 if not present */ - int type; - /** The sample location. 0 if unknown */ - int sampleLocation; - /** Sensor status annunciation flags. 0 if not present */ - int status; - - protected MeasurementContext context; - - @SuppressWarnings("unused") - static class MeasurementContext { - static final int UNIT_kg = 0; - static final int UNIT_l = 1; - - /** - * One of the following:
- * 0 Not present
- * 1 Breakfast
- * 2 Lunch
- * 3 Dinner
- * 4 Snack
- * 5 Drink
- * 6 Supper
- * 7 Brunch - */ - int carbohydrateId; - /** Number of kilograms of carbohydrate */ - float carbohydrateUnits; - /** - * One of the following:
- * 0 Not present
- * 1 Preprandial (before meal)
- * 2 Postprandial (after meal)
- * 3 Fasting
- * 4 Casual (snacks, drinks, etc.)
- * 5 Bedtime - */ - int meal; - /** - * One of the following:
- * 0 Not present
- * 1 Self
- * 2 Health Care Professional
- * 3 Lab test
- * 15 Tester value not available - */ - int tester; - /** - * One of the following:
- * 0 Not present
- * 1 Minor health issues
- * 2 Major health issues
- * 3 During menses
- * 4 Under stress
- * 5 No health issues
- * 15 Tester value not available - */ - int health; - /** Exercise duration in seconds. 0 if not present */ - int exerciseDuration; - /** Exercise intensity in percent. 0 if not present */ - int exerciseIntensity; - /** - * One of the following:
- * 0 Not present
- * 1 Rapid acting insulin
- * 2 Short acting insulin
- * 3 Intermediate acting insulin
- * 4 Long acting insulin
- * 5 Pre-mixed insulin - */ - int medicationId; - /** Quantity of medication. See {@link #medicationUnit} for the unit. */ - float medicationQuantity; - /** One of the following: {@link GlucoseRecord.MeasurementContext#UNIT_kg}, {@link GlucoseRecord.MeasurementContext#UNIT_l}. */ - int medicationUnit; - /** HbA1c value. 0 if not present */ - float HbA1c; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRActivity.java deleted file mode 100644 index 2ad3a50f..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRActivity.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (c) 2015, 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.hr; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.Intent; -import android.graphics.Point; -import android.os.Bundle; -import android.os.Handler; - -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.view.ViewGroup; -import android.widget.TextView; - -import org.achartengine.GraphicalView; - -import java.util.List; -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileActivity; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -/** - * HRSActivity is the main Heart rate activity. It implements HRSManagerCallbacks to receive callbacks from HRSManager class. The activity supports portrait and landscape orientations. The activity - * uses external library AChartEngine to show real time graph of HR values. - */ -// TODO The HRSActivity should be rewritten to use the service approach, like other do. -public class HRActivity extends BleProfileActivity implements HRManagerCallbacks { - @SuppressWarnings("unused") - private final String TAG = "HRSActivity"; - - private final static String GRAPH_STATUS = "graph_status"; - private final static String GRAPH_COUNTER = "graph_counter"; - private final static String HR_VALUE = "hr_value"; - - private final static int REFRESH_INTERVAL = 1000; // 1 second interval - - private Handler handler = new Handler(); - - private boolean isGraphInProgress = false; - - private GraphicalView graphView; - private LineGraphView lineGraph; - private TextView hrValueView, hrLocationView; - private TextView batteryLevelView; - - private int hrValue = 0; - private int counter = 0; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_hrs); - setGUI(); - } - - private void setGUI() { - lineGraph = LineGraphView.getLineGraphView(); - hrValueView = findViewById(R.id.text_hrs_value); - hrLocationView = findViewById(R.id.text_hrs_position); - batteryLevelView = findViewById(R.id.battery); - showGraph(); - } - - private void showGraph() { - graphView = lineGraph.getView(this); - ViewGroup layout = findViewById(R.id.graph_hrs); - layout.addView(graphView); - } - - @Override - protected void onStart() { - super.onStart(); - - final Intent intent = getIntent(); - if (!isDeviceConnected() && intent.hasExtra(FeaturesActivity.EXTRA_ADDRESS)) { - final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - final BluetoothDevice device = bluetoothAdapter.getRemoteDevice(getIntent().getByteArrayExtra(FeaturesActivity.EXTRA_ADDRESS)); - onDeviceSelected(device, device.getName()); - - intent.removeExtra(FeaturesActivity.EXTRA_APP); - intent.removeExtra(FeaturesActivity.EXTRA_ADDRESS); - } - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - isGraphInProgress = savedInstanceState.getBoolean(GRAPH_STATUS); - counter = savedInstanceState.getInt(GRAPH_COUNTER); - hrValue = savedInstanceState.getInt(HR_VALUE); - - if (isGraphInProgress) - startShowGraph(); - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putBoolean(GRAPH_STATUS, isGraphInProgress); - outState.putInt(GRAPH_COUNTER, counter); - outState.putInt(HR_VALUE, hrValue); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - stopShowGraph(); - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.hrs_feature_title; - } - - @Override - protected int getAboutTextId() { - return R.string.hrs_about_text; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.hrs_default_name; - } - - @Override - protected UUID getFilterUUID() { - return HRManager.HR_SERVICE_UUID; - } - - private void updateGraph(final int hrmValue) { - counter++; - lineGraph.addValue(new Point(counter, hrmValue)); - graphView.repaint(); - } - - private Runnable repeatTask = new Runnable() { - @Override - public void run() { - if (hrValue > 0) - updateGraph(hrValue); - if (isGraphInProgress) - handler.postDelayed(repeatTask, REFRESH_INTERVAL); - } - }; - - void startShowGraph() { - isGraphInProgress = true; - repeatTask.run(); - } - - void stopShowGraph() { - isGraphInProgress = false; - handler.removeCallbacks(repeatTask); - } - - @Override - protected LoggableBleManager initializeManager() { - final HRManager manager = HRManager.getInstance(getApplicationContext()); - manager.setGattCallbacks(this); - return manager; - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // this may notify user or show some views - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - startShowGraph(); - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - runOnUiThread(() -> this.batteryLevelView.setText(getString(R.string.battery, batteryLevel))); - } - - @Override - public void onBodySensorLocationReceived(@NonNull final BluetoothDevice device, final int sensorLocation) { - runOnUiThread(() -> { - if (sensorLocation >= SENSOR_LOCATION_FIRST && sensorLocation <= SENSOR_LOCATION_LAST) { - hrLocationView.setText(getResources().getStringArray(R.array.hrs_locations)[sensorLocation]); - } else { - hrLocationView.setText(R.string.hrs_location_other); - } - }); - } - - @Override - public void onHeartRateMeasurementReceived(@NonNull final BluetoothDevice device, - @IntRange(from = 0) final int heartRate, - @Nullable final Boolean contactDetected, - @Nullable @IntRange(from = 0) final Integer energyExpanded, - @Nullable final List rrIntervals) { - hrValue = heartRate; - runOnUiThread(() -> hrValueView.setText(getString(R.string.hrs_value, heartRate))); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - runOnUiThread(() -> { - hrValueView.setText(R.string.not_available_value); - hrLocationView.setText(R.string.not_available); - batteryLevelView.setText(R.string.not_available); - stopShowGraph(); - }); - } - - @Override - protected void setDefaultUI() { - hrValueView.setText(R.string.not_available_value); - hrLocationView.setText(R.string.not_available); - batteryLevelView.setText(R.string.not_available); - clearGraph(); - } - - private void clearGraph() { - lineGraph.clearGraph(); - graphView.repaint(); - counter = 0; - hrValue = 0; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManager.java deleted file mode 100644 index f542a8b1..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManager.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) 2015, 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.hr; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; - -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.util.Log; - -import java.util.List; -import java.util.UUID; - -import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallback; -import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback; -import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocation; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.BodySensorLocationParser; -import no.nordicsemi.android.nrftoolbox.parser.HeartRateMeasurementParser; - -/** - * HRSManager class performs BluetoothGatt operations for connection, service discovery, - * enabling notification and reading characteristics. - * All operations required to connect to device with BLE Heart Rate Service and reading - * heart rate values are performed here. - */ -public class HRManager extends BatteryManager { - static final UUID HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb"); - private static final UUID BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb"); - private static final UUID HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb"); - - private BluetoothGattCharacteristic heartRateCharacteristic, bodySensorLocationCharacteristic; - - private static HRManager managerInstance = null; - - /** - * Singleton implementation of HRSManager class. - */ - public static synchronized HRManager getInstance(final Context context) { - if (managerInstance == null) { - managerInstance = new HRManager(context); - } - return managerInstance; - } - - private HRManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new HeartRateManagerCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving notification, etc. - */ - private final class HeartRateManagerCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - super.initialize(); - readCharacteristic(bodySensorLocationCharacteristic) - .with(new BodySensorLocationDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + BodySensorLocationParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @Override - public void onBodySensorLocationReceived(@NonNull final BluetoothDevice device, - @BodySensorLocation final int sensorLocation) { - mCallbacks.onBodySensorLocationReceived(device, sensorLocation); - } - }) - .fail((device, status) -> log(Log.WARN, "Body Sensor Location characteristic not found")) - .enqueue(); - setNotificationCallback(heartRateCharacteristic) - .with(new HeartRateMeasurementDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + HeartRateMeasurementParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @Override - public void onHeartRateMeasurementReceived(@NonNull final BluetoothDevice device, - @IntRange(from = 0) final int heartRate, - @Nullable final Boolean contactDetected, - @Nullable @IntRange(from = 0) final Integer energyExpanded, - @Nullable final List rrIntervals) { - mCallbacks.onHeartRateMeasurementReceived(device, heartRate, contactDetected, energyExpanded, rrIntervals); - } - }); - enableNotifications(heartRateCharacteristic).enqueue(); - } - - @Override - protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(HR_SERVICE_UUID); - if (service != null) { - heartRateCharacteristic = service.getCharacteristic(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID); - } - return heartRateCharacteristic != null; - } - - @Override - protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) { - super.isOptionalServiceSupported(gatt); - final BluetoothGattService service = gatt.getService(HR_SERVICE_UUID); - if (service != null) { - bodySensorLocationCharacteristic = service.getCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID); - } - return bodySensorLocationCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - super.onDeviceDisconnected(); - bodySensorLocationCharacteristic = null; - heartRateCharacteristic = null; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManagerCallbacks.java deleted file mode 100644 index 7e02480d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/HRManagerCallbacks.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2015, 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.hr; - -import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocationCallback; -import no.nordicsemi.android.ble.common.profile.hr.HeartRateMeasurementCallback; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -interface HRManagerCallbacks extends BatteryManagerCallbacks, BodySensorLocationCallback, HeartRateMeasurementCallback { - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/LineGraphView.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/LineGraphView.java deleted file mode 100644 index 4793f01b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/hr/LineGraphView.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2015, 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.hr; - -import android.content.Context; -import android.graphics.Color; -import android.graphics.Paint.Align; -import android.graphics.Point; - -import org.achartengine.ChartFactory; -import org.achartengine.GraphicalView; -import org.achartengine.chart.PointStyle; -import org.achartengine.model.TimeSeries; -import org.achartengine.model.XYMultipleSeriesDataset; -import org.achartengine.renderer.XYMultipleSeriesRenderer; -import org.achartengine.renderer.XYSeriesRenderer; - -import androidx.annotation.NonNull; - -/** - * This class uses external library AChartEngine to show dynamic real time line graph for HR values - */ -class LineGraphView { - //TimeSeries will hold the data in x,y format for single chart - private TimeSeries series = new TimeSeries("Heart Rate"); - //XYMultipleSeriesDataset will contain all the TimeSeries - private XYMultipleSeriesDataset dataSet = new XYMultipleSeriesDataset(); - //XYMultipleSeriesRenderer will contain all XYSeriesRenderer and it can be used to set the properties of whole Graph - private XYMultipleSeriesRenderer multiRenderer = new XYMultipleSeriesRenderer(); - private static LineGraphView instance = null; - - /** - * Singleton implementation of LineGraphView class - */ - @NonNull - static synchronized LineGraphView getLineGraphView() { - if (instance == null) { - instance = new LineGraphView(); - } - return instance; - } - - /** - * This constructor will set some properties of single chart and some properties of whole graph - */ - private LineGraphView() { - //add single line chart series - dataSet.addSeries(series); - - //XYSeriesRenderer is used to set the properties like chart color, style of each point, etc. of single chart - final XYSeriesRenderer seriesRenderer = new XYSeriesRenderer(); - //set line chart color to Black - seriesRenderer.setColor(Color.BLACK); - //set line chart style to square points - seriesRenderer.setPointStyle(PointStyle.SQUARE); - seriesRenderer.setFillPoints(true); - - final XYMultipleSeriesRenderer renderer = multiRenderer; - //set whole graph background color to transparent color - renderer.setBackgroundColor(Color.TRANSPARENT); - renderer.setMargins(new int[] { 50, 65, 40, 5 }); // top, left, bottom, right - renderer.setMarginsColor(Color.argb(0x00, 0x01, 0x01, 0x01)); - renderer.setAxesColor(Color.BLACK); - renderer.setAxisTitleTextSize(24); - renderer.setShowGrid(true); - renderer.setGridColor(Color.LTGRAY); - renderer.setLabelsColor(Color.BLACK); - renderer.setYLabelsColor(0, Color.DKGRAY); - renderer.setYLabelsAlign(Align.RIGHT); - renderer.setYLabelsPadding(4.0f); - renderer.setXLabelsColor(Color.DKGRAY); - renderer.setLabelsTextSize(20); - renderer.setLegendTextSize(20); - //Disable zoom - renderer.setPanEnabled(false, false); - renderer.setZoomEnabled(false, false); - //set title to x-axis and y-axis - renderer.setXTitle(" Time (seconds)"); - renderer.setYTitle(" BPM"); - renderer.addSeriesRenderer(seriesRenderer); - } - - /** - * return graph view to activity - */ - GraphicalView getView(@NonNull final Context context) { - return ChartFactory.getLineChartView(context, dataSet, multiRenderer); - } - - /** - * add new x,y value to chart - */ - void addValue(@NonNull final Point p) { - series.add(p.x, p.y); - } - - /** - * clear all previous values of chart - */ - void clearGraph() { - series.clear(); - } - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTActivity.java deleted file mode 100644 index 137c6f1b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTActivity.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (c) 2015, 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.ht; - -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; - -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.view.Menu; -import android.widget.TextView; - -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ht.settings.SettingsActivity; -import no.nordicsemi.android.nrftoolbox.ht.settings.SettingsFragment; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; - -/** - * HTSActivity is the main Health Thermometer activity. It implements {@link HTManagerCallbacks} - * to receive callbacks from {@link HTManager} class. The activity supports portrait and landscape - * orientations. - */ -public class HTActivity extends BleProfileServiceReadyActivity { - @SuppressWarnings("unused") - private final String TAG = "HTSActivity"; - - private TextView tempValueView; - private TextView unitView; - private TextView batteryLevelView; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_hts); - setGUI(); - } - - @Override - protected void onInitialize(final Bundle savedInstanceState) { - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, makeIntentFilter()); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); - } - - private void setGUI() { - tempValueView = findViewById(R.id.text_hts_value); - unitView = findViewById(R.id.text_hts_unit); - batteryLevelView = findViewById(R.id.battery); - } - - @Override - protected void onResume() { - super.onResume(); - setUnits(); - } - - @Override - protected void setDefaultUI() { - tempValueView.setText(R.string.not_available_value); - batteryLevelView.setText(R.string.not_available); - - setUnits(); - } - - private void setUnits() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT))); - - switch (unit) { - case SettingsFragment.SETTINGS_UNIT_C: - this.unitView.setText(R.string.hts_unit_celsius); - break; - case SettingsFragment.SETTINGS_UNIT_F: - this.unitView.setText(R.string.hts_unit_fahrenheit); - break; - case SettingsFragment.SETTINGS_UNIT_K: - this.unitView.setText(R.string.hts_unit_kelvin); - break; - } - } - - @Override - protected void onServiceBound(final HTService.HTSBinder binder) { - onTemperatureMeasurementReceived(binder.getTemperature()); - } - - @Override - protected void onServiceUnbound() { - // not used - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.hts_feature_title; - } - - @Override - protected int getAboutTextId() { - return R.string.hts_about_text; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.settings_and_about, menu); - return true; - } - - @Override - protected boolean onOptionsItemSelected(final int itemId) { - switch (itemId) { - case R.id.action_settings: - final Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - break; - } - return true; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.hts_default_name; - } - - @Override - protected UUID getFilterUUID() { - return HTManager.HT_SERVICE_UUID; - } - - @Override - protected Class getServiceClass() { - return HTService.class; - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, boolean optionalServicesFound) { - // this may notify user or show some views - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - batteryLevelView.setText(R.string.not_available); - } - - private void onTemperatureMeasurementReceived(Float value) { - if (value != null) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, - String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT))); - - switch (unit) { - case SettingsFragment.SETTINGS_UNIT_F: - value = value * 1.8f + 32f; - break; - case SettingsFragment.SETTINGS_UNIT_K: - value += 273.15f; - break; - case SettingsFragment.SETTINGS_UNIT_C: - break; - } - tempValueView.setText(getString(R.string.hts_value, value)); - } else { - tempValueView.setText(R.string.not_available_value); - } - } - - public void onBatteryLevelChanged(final int value) { - batteryLevelView.setText(getString(R.string.battery, value)); - } - - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - - if (HTService.BROADCAST_HTS_MEASUREMENT.equals(action)) { - final float value = intent.getFloatExtra(HTService.EXTRA_TEMPERATURE, 0.0f); - // Update GUI - onTemperatureMeasurementReceived(value); - } else if (HTService.BROADCAST_BATTERY_LEVEL.equals(action)) { - final int batteryLevel = intent.getIntExtra(HTService.EXTRA_BATTERY_LEVEL, 0); - // Update GUI - onBatteryLevelChanged(batteryLevel); - } - } - }; - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(HTService.BROADCAST_HTS_MEASUREMENT); - intentFilter.addAction(HTService.BROADCAST_BATTERY_LEVEL); - return intentFilter; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManager.java deleted file mode 100644 index 65d6818d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManager.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2015, 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.ht; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Calendar; -import java.util.UUID; - -import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCallback; -import no.nordicsemi.android.ble.common.profile.ht.TemperatureType; -import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.TemperatureMeasurementParser; - -/** - * {@link HTManager} class performs {@link BluetoothGatt} operations for connection, service discovery, - * enabling indication and reading characteristics. All operations required to connect to device - * with BLE HT Service and reading health thermometer values are performed here. - * {@link HTActivity} implements {@link HTManagerCallbacks} in order to receive callbacks of - * {@link BluetoothGatt} operations. - */ -public class HTManager extends BatteryManager { - /** Health Thermometer service UUID */ - final static UUID HT_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb"); - /** Health Thermometer Measurement characteristic UUID */ - private static final UUID HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb"); - - private BluetoothGattCharacteristic htCharacteristic; - - HTManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new HTManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc.. - */ - private class HTManagerGattCallback extends BatteryManagerGattCallback { - @Override - protected void initialize() { - super.initialize(); - setIndicationCallback(htCharacteristic) - .with(new TemperatureMeasurementDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + TemperatureMeasurementParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @Override - public void onTemperatureMeasurementReceived(@NonNull final BluetoothDevice device, - final float temperature, - @TemperatureUnit final int unit, - @Nullable final Calendar calendar, - @Nullable @TemperatureType final Integer type) { - mCallbacks.onTemperatureMeasurementReceived(device, temperature, unit, calendar, type); - } - }); - enableIndications(htCharacteristic).enqueue(); - } - - @Override - protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(HT_SERVICE_UUID); - if (service != null) { - htCharacteristic = service.getCharacteristic(HT_MEASUREMENT_CHARACTERISTIC_UUID); - } - return htCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - super.onDeviceDisconnected(); - htCharacteristic = null; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManagerCallbacks.java deleted file mode 100644 index e35856d5..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTManagerCallbacks.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2015, 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.ht; - -import no.nordicsemi.android.ble.common.profile.ht.TemperatureMeasurementCallback; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -/** - * Interface {@link HTManagerCallbacks} must be implemented by {@link HTActivity} in order - * to receive callbacks from {@link HTManager}. - */ -interface HTManagerCallbacks extends BatteryManagerCallbacks, TemperatureMeasurementCallback { - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTService.java deleted file mode 100644 index a87d5adf..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/HTService.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright (c) 2015, 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.ht; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - -import java.util.Calendar; - -import no.nordicsemi.android.ble.common.profile.ht.TemperatureMeasurementCallback; -import no.nordicsemi.android.ble.common.profile.ht.TemperatureType; -import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -@SuppressWarnings("FieldCanBeLocal") -public class HTService extends BleProfileService implements HTManagerCallbacks { - public static final String BROADCAST_HTS_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.hts.BROADCAST_HTS_MEASUREMENT"; - public static final String EXTRA_TEMPERATURE = "no.nordicsemi.android.nrftoolbox.hts.EXTRA_TEMPERATURE"; - - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - - private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.hts.ACTION_DISCONNECT"; - - private final static int NOTIFICATION_ID = 267; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; - /** The last received temperature value in Celsius degrees. */ - private Float temp; - - @SuppressWarnings("unused") - private HTManager manager; - - private final LocalBinder minder = new HTSBinder(); - - /** - * This local binder is an interface for the bonded activity to operate with the HTS sensor - */ - class HTSBinder extends LocalBinder { - /** - * Returns the last received temperature value. - * - * @return Temperature value in Celsius. - */ - Float getTemperature() { - return temp; - } - } - - @Override - protected LocalBinder getBinder() { - return minder; - } - - @Override - protected LoggableBleManager initializeManager() { - return manager = new HTManager(this); - } - - @Override - public void onCreate() { - super.onCreate(); - - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(disconnectActionBroadcastReceiver, filter); - } - - @Override - public void onDestroy() { - // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - cancelNotification(); - unregisterReceiver(disconnectActionBroadcastReceiver); - - super.onDestroy(); - } - - @Override - protected void onRebind() { - stopForegroundService(); - } - - @Override - protected void onUnbind() { - startForegroundService(); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - temp = null; - } - - @Override - public void onTemperatureMeasurementReceived(@NonNull final BluetoothDevice device, - final float temperature, @TemperatureUnit final int unit, - @Nullable final Calendar calendar, - @Nullable @TemperatureType final Integer type) { - temp = TemperatureMeasurementCallback.toCelsius(temperature, unit); - - final Intent broadcast = new Intent(BROADCAST_HTS_MEASUREMENT); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_TEMPERATURE, temp); - // ignore the rest - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - if (!bound) { - // Here we may update the notification to display the current temperature. - // TODO modify the notification here - } - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Sets the service as a foreground service - */ - private void startForegroundService(){ - // when the activity closes we need to show the notification that user is connected to the peripheral sensor - // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services - final Notification notification = createNotification(R.string.hts_notification_connected_message, 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - } - - /** - * Stops the service as a foreground service - */ - private void 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) { - stopForeground(true); - } else { - cancelNotification(); - } - } - - /** - * Creates the notification - * @param messageResId - * message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - */ - @SuppressWarnings("SameParameterValue") - private Notification createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, HTActivity.class); - - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - - // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[] { parentIntent, targetIntent }, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); - builder.setContentIntent(pendingIntent); - builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); - builder.setSmallIcon(R.drawable.ic_stat_notify_hts); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.hts_notification_action_disconnect), disconnectAction)); - - return builder.build(); - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } - - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver disconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsActivity.java deleted file mode 100644 index 9830868f..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsActivity.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2015, 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.ht.settings; - -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.MenuItem; - -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsActivity extends AppCompatActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Display the fragment as the main content. - getSupportFragmentManager().beginTransaction().replace(R.id.content, new SettingsFragment()).commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsFragment.java deleted file mode 100644 index 316e7cc9..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ht/settings/SettingsFragment.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2015, 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.ht.settings; - -import android.os.Bundle; - -import androidx.preference.PreferenceFragmentCompat; -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsFragment extends PreferenceFragmentCompat { - public static final String SETTINGS_UNIT = "settings_hts_unit"; - public static final int SETTINGS_UNIT_C = 0; // [C] - public static final int SETTINGS_UNIT_F = 1; // [F] - public static final int SETTINGS_UNIT_K = 2; // [K] - public static final int SETTINGS_UNIT_DEFAULT = SETTINGS_UNIT_C; - - @Override - public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.settings_hts); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/AlertLevelParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/AlertLevelParser.java deleted file mode 100644 index 68ff78b1..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/AlertLevelParser.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import android.bluetooth.BluetoothGattCharacteristic; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class AlertLevelParser { - public static String parse(final BluetoothGattCharacteristic characteristic) { - return parse(Data.from(characteristic)); - } - - /** - * Parses the alert level. - * - * @param data - * @return alert level in human readable format - */ - public static String parse(final Data data) { - final int value = data.getIntValue(Data.FORMAT_UINT8, 0); - - switch (value) { - case 0: - return "No Alert"; - case 1: - return "Mild Alert"; - case 2: - return "High Alert"; - default: - return "Reserved value (" + value + ")"; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BloodPressureMeasurementParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BloodPressureMeasurementParser.java deleted file mode 100644 index 9351730a..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BloodPressureMeasurementParser.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class BloodPressureMeasurementParser { - - public static String parse(final Data data) { - final StringBuilder builder = new StringBuilder(); - - // first byte - flags - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); - - final int unitType = flags & 0x01; - final boolean timestampPresent = (flags & 0x02) > 0; - final boolean pulseRatePresent = (flags & 0x04) > 0; - final boolean userIdPresent = (flags & 0x08) > 0; - final boolean statusPresent = (flags & 0x10) > 0; - - // following bytes - systolic, diastolic and mean arterial pressure - final float systolic = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - final float diastolic = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 2); - final float meanArterialPressure = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 4); - final String unit = unitType == 0 ? " mmHg" : " kPa"; - offset += 6; - builder.append("Systolic: ").append(systolic).append(unit); - builder.append("\nDiastolic: ").append(diastolic).append(unit); - builder.append("\nMean AP: ").append(meanArterialPressure).append(unit); - - // parse timestamp if present - if (timestampPresent) { - builder.append("\nTimestamp: ").append(DateTimeParser.parse(data, offset)); - offset += 7; - } - - // parse pulse rate if present - if (pulseRatePresent) { - final float pulseRate = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - offset += 2; - builder.append("\nPulse: ").append(pulseRate).append(" bpm"); - } - - if (userIdPresent) { - final int userId = data.getIntValue(Data.FORMAT_UINT8, offset); - offset += 1; - builder.append("\nUser ID: ").append(userId); - } - - if (statusPresent) { - final int status = data.getIntValue(Data.FORMAT_UINT16, offset); - // offset += 2; - if ((status & 0x0001) > 0) - builder.append("\nBody movement detected"); - if ((status & 0x0002) > 0) - builder.append("\nCuff too lose"); - if ((status & 0x0004) > 0) - builder.append("\nIrregular pulse detected"); - if ((status & 0x0018) == 0x0008) - builder.append("\nPulse rate exceeds upper limit"); - if ((status & 0x0018) == 0x0010) - builder.append("\nPulse rate is less than lower limit"); - if ((status & 0x0018) == 0x0018) - builder.append("\nPulse rate range: Reserved for future use "); - if ((status & 0x0020) > 0) - builder.append("\nImproper measurement position"); - } - - return builder.toString(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BodySensorLocationParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BodySensorLocationParser.java deleted file mode 100644 index 22d17312..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/BodySensorLocationParser.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class BodySensorLocationParser { - - public static String parse(final Data data) { - final int value = data.getIntValue(Data.FORMAT_UINT8, 0); - - switch (value) { - case 6: return "Foot"; - case 5: return "Ear Lobe"; - case 4: return "Hand"; - case 3: return "Finger"; - case 2: return "Wrist"; - case 1: return "Chest"; - case 0: - default: return "Other"; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMMeasurementParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMMeasurementParser.java deleted file mode 100644 index 6c9c8075..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMMeasurementParser.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import java.util.Locale; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class CGMMeasurementParser { - private static final int FLAGS_CGM_TREND_INFO_PRESENT = 1; - private static final int FLAGS_CGM_QUALITY_PRESENT = 1 << 1; - private static final int FLAGS_SENSOR_STATUS_ANNUNCIATION_WARNING_OCTET_PRESENT = 1 << 2; - private static final int FLAGS_SENSOR_STATUS_ANNUNCIATION_CAL_TEMP_OCTET_PRESENT = 1 << 3; - private static final int FLAGS_SENSOR_STATUS_ANNUNCIATION_STATUS_OCTET_PRESENT = 1 << 4; - - private static final int SSA_SESSION_STOPPED = 1; - private static final int SSA_DEVICE_BATTERY_LOW = 1 << 1; - private static final int SSA_SENSOR_TYPE_INCORRECT = 1 << 2; - private static final int SSA_SENSOR_MALFUNCTION = 1 << 3; - private static final int SSA_DEVICE_SPEC_ALERT = 1 << 4; - private static final int SSA_GENERAL_DEVICE_FAULT = 1 << 5; - - private static final int SSA_TIME_SYNC_REQUIRED = 1 << 8; - private static final int SSA_CALIBRATION_NOT_ALLOWED = 1 << 9; - private static final int SSA_CALIBRATION_RECOMMENDED = 1 << 10; - private static final int SSA_CALIBRATION_REQUIRED = 1 << 11; - private static final int SSA_SENSOR_TEMP_TOO_HIGH = 1 << 12; - private static final int SSA_SENSOR_TEMP_TOO_LOW = 1 << 13; - - private static final int SSA_RESULT_LOWER_THAN_PATIENT_LOW_LEVEL = 1 << 16; - private static final int SSA_RESULT_HIGHER_THAN_PATIENT_HIGH_LEVEL = 1 << 17; - private static final int SSA_RESULT_LOWER_THAN_HYPO_LEVEL = 1 << 18; - private static final int SSA_RESULT_HIGHER_THAN_HYPER_LEVEL = 1 << 19; - private static final int SSA_SENSOR_RATE_OF_DECREASE_EXCEEDED = 1 << 20; - private static final int SSA_SENSOR_RATE_OF_INCREASE_EXCEEDED = 1 << 21; - private static final int SSA_RESULT_LOWER_THAN_DEVICE_CAN_PROCESS = 1 << 22; - private static final int SSA_RESULT_HIGHER_THAN_DEVICE_CAN_PROCESS = 1 << 23; - - public static String parse(final Data data) { - // The CGM Measurement characteristic is a variable length structure containing one or more CGM Measurement records - int totalSize = data.getValue().length; - - final StringBuilder builder = new StringBuilder(); - int offset = 0; - while (offset < totalSize) { - offset += parseRecord(builder, data, offset); - if (offset < totalSize) - builder.append("\n\n"); - } - return builder.toString(); - } - - private static int parseRecord(final StringBuilder builder, final Data data, int offset) { - // Read size and flags bytes - final int size = data.getIntValue(Data.FORMAT_UINT8, offset++); - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); - - /* - * false CGM Trend Information is not preset - * true CGM Trend Information is preset - */ - final boolean cgmTrendInformationPresent = (flags & FLAGS_CGM_TREND_INFO_PRESENT) > 0; - - /* - * false CGM Quality is not preset - * true CGM Quality is preset - */ - final boolean cgmQualityPresent = (flags & FLAGS_CGM_QUALITY_PRESENT) > 0; - - /* - * false Sensor Status Annunciation - Warning-Octet is not preset - * true Sensor Status Annunciation - Warning-Octet is preset - */ - final boolean ssaWarningOctetPresent = (flags & FLAGS_SENSOR_STATUS_ANNUNCIATION_WARNING_OCTET_PRESENT) > 0; - - /* - * false Sensor Status Annunciation - Calibration/Temp-Octet is not preset - * true Sensor Status Annunciation - Calibration/Temp-Octet is preset - */ - final boolean ssaCalTempOctetPresent = (flags & FLAGS_SENSOR_STATUS_ANNUNCIATION_CAL_TEMP_OCTET_PRESENT) > 0; - - /* - * false Sensor Status Annunciation - Status-Octet is not preset - * true Sensor Status Annunciation - Status-Octet is preset - */ - final boolean ssaStatusOctetPresent = (flags & FLAGS_SENSOR_STATUS_ANNUNCIATION_STATUS_OCTET_PRESENT) > 0; - - // Read CGM Glucose Concentration - final float glucoseConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - offset += 2; - - // Read time offset - final int timeOffset = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - - builder.append("Glucose concentration: ").append(glucoseConcentration).append(" mg/dL\n"); - builder.append("Sequence number: ").append(timeOffset).append(" (Time Offset in min)\n"); - - if (ssaWarningOctetPresent) { - final int ssaWarningOctet = data.getIntValue(Data.FORMAT_UINT8, offset++); - builder.append("Warnings:\n"); - if ((ssaWarningOctet & SSA_SESSION_STOPPED) > 0) - builder.append("- Session Stopped\n"); - if ((ssaWarningOctet & SSA_DEVICE_BATTERY_LOW) > 0) - builder.append("- Device Battery Low\n"); - if ((ssaWarningOctet & SSA_SENSOR_TYPE_INCORRECT) > 0) - builder.append("- Sensor Type Incorrect\n"); - if ((ssaWarningOctet & SSA_SENSOR_MALFUNCTION) > 0) - builder.append("- Sensor Malfunction\n"); - if ((ssaWarningOctet & SSA_DEVICE_SPEC_ALERT) > 0) - builder.append("- Device Specific Alert\n"); - if ((ssaWarningOctet & SSA_GENERAL_DEVICE_FAULT) > 0) - builder.append("- General Device Fault\n"); - } - - if (ssaCalTempOctetPresent) { - final int ssaCalTempOctet = data.getIntValue(Data.FORMAT_UINT8, offset++); - builder.append("Cal/Temp Info:\n"); - if ((ssaCalTempOctet & SSA_TIME_SYNC_REQUIRED) > 0) - builder.append("- Time Synchronization Required\n"); - if ((ssaCalTempOctet & SSA_CALIBRATION_NOT_ALLOWED) > 0) - builder.append("- Calibration Not Allowed\n"); - if ((ssaCalTempOctet & SSA_CALIBRATION_RECOMMENDED) > 0) - builder.append("- Calibration Recommended\n"); - if ((ssaCalTempOctet & SSA_CALIBRATION_REQUIRED) > 0) - builder.append("- Calibration Required\n"); - if ((ssaCalTempOctet & SSA_SENSOR_TEMP_TOO_HIGH) > 0) - builder.append("- Sensor Temp Too High\n"); - if ((ssaCalTempOctet & SSA_SENSOR_TEMP_TOO_LOW) > 0) - builder.append("- Sensor Temp Too Low\n"); - } - - if (ssaStatusOctetPresent) { - final int ssaStatusOctet = data.getIntValue(Data.FORMAT_UINT8, offset++); - builder.append("Status:\n"); - if ((ssaStatusOctet & SSA_RESULT_LOWER_THAN_PATIENT_LOW_LEVEL) > 0) - builder.append("- Result Lower then Patient Low Level\n"); - if ((ssaStatusOctet & SSA_RESULT_HIGHER_THAN_PATIENT_HIGH_LEVEL) > 0) - builder.append("- Result Higher then Patient High Level\n"); - if ((ssaStatusOctet & SSA_RESULT_LOWER_THAN_HYPO_LEVEL) > 0) - builder.append("- Result Lower then Hypo Level\n"); - if ((ssaStatusOctet & SSA_RESULT_HIGHER_THAN_HYPER_LEVEL) > 0) - builder.append("- Result Higher then Hyper Level\n"); - if ((ssaStatusOctet & SSA_SENSOR_RATE_OF_DECREASE_EXCEEDED) > 0) - builder.append("- Sensor Rate of Decrease Exceeded\n"); - if ((ssaStatusOctet & SSA_SENSOR_RATE_OF_INCREASE_EXCEEDED) > 0) - builder.append("- Sensor Rate of Increase Exceeded\n"); - if ((ssaStatusOctet & SSA_RESULT_LOWER_THAN_DEVICE_CAN_PROCESS) > 0) - builder.append("- Result Lower then Device Can Process\n"); - if ((ssaStatusOctet & SSA_RESULT_HIGHER_THAN_DEVICE_CAN_PROCESS) > 0) - builder.append("- Result Higher then Device Can Process\n"); - } - - if (cgmTrendInformationPresent) { - final float trend = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - offset += 2; - builder.append("Trend: ").append(trend).append(" mg/dL/min\n"); - } - - if (cgmQualityPresent) { - final float quality = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - offset += 2; - builder.append("Quality: ").append(quality).append("%\n"); - } - - if (size > offset + 1) { - final int crc = data.getIntValue(Data.FORMAT_UINT16, offset); - // offset += 2; - builder.append(String.format(Locale.US, "E2E-CRC: 0x%04X\n", crc)); - } - builder.setLength(builder.length() - 1); // Remove last \n - return size; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMSpecificOpsControlPointParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMSpecificOpsControlPointParser.java deleted file mode 100644 index c8afc88a..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CGMSpecificOpsControlPointParser.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class CGMSpecificOpsControlPointParser { - private final static int OP_SET_CGM_COMMUNICATION_INTERVAL = 1; - private final static int OP_GET_CGM_COMMUNICATION_INTERVAL = 2; - private final static int OP_CGM_COMMUNICATION_INTERVAL_RESPONSE = 3; - private final static int OP_SET_GLUCOSE_CALIBRATION_VALUE = 4; - private final static int OP_GET_GLUCOSE_CALIBRATION_VALUE = 5; - private final static int OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE = 6; - private final static int OP_SET_PATIENT_HIGH_ALERT_LEVEL = 7; - private final static int OP_GET_PATIENT_HIGH_ALERT_LEVEL = 8; - private final static int OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE = 9; - private final static int OP_SET_PATIENT_LOW_ALERT_LEVEL = 10; - private final static int OP_GET_PATIENT_LOW_ALERT_LEVEL = 11; - private final static int OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE = 12; - private final static int OP_SET_HYPO_ALERT_LEVEL = 13; - private final static int OP_GET_HYPO_ALERT_LEVEL = 14; - private final static int OP_HYPO_ALERT_LEVEL_RESPONSE = 15; - private final static int OP_SET_HYPER_ALERT_LEVEL = 16; - private final static int OP_GET_HYPER_ALERT_LEVEL = 17; - private final static int OP_HYPER_ALERT_LEVEL_RESPONSE = 18; - private final static int OP_SET_RATE_OF_DECREASE_ALERT_LEVEL = 19; - private final static int OP_GET_RATE_OF_DECREASE_ALERT_LEVEL = 20; - private final static int OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE = 21; - private final static int OP_SET_RATE_OF_INCREASE_ALERT_LEVEL = 22; - private final static int OP_GET_RATE_OF_INCREASE_ALERT_LEVEL = 23; - private final static int OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE = 24; - private final static int OP_RESET_DEVICE_SPECIFIC_ALERT = 25; - private final static int OP_CODE_START_SESSION = 26; - private final static int OP_CODE_STOP_SESSION = 27; - private final static int OP_CODE_RESPONSE_CODE = 28; - - // TODO this parser does not support E2E-CRC! - - public static String parse(final Data data) { - int offset = 0; - final int opCode = data.getIntValue(Data.FORMAT_UINT8, offset++); - - final StringBuilder builder = new StringBuilder(); - builder.append(parseOpCode(opCode)); - switch (opCode) { - case OP_SET_CGM_COMMUNICATION_INTERVAL: - case OP_CGM_COMMUNICATION_INTERVAL_RESPONSE: { - final int interval = data.getIntValue(Data.FORMAT_UINT8, offset); - builder.append(" to ").append(interval).append(" min"); - break; - } - case OP_SET_GLUCOSE_CALIBRATION_VALUE: { - final float calConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - offset += 2; - final int calTime = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - final int calTypeSampleLocation = data.getIntValue(Data.FORMAT_UINT8, offset++); - final int calType = calTypeSampleLocation & 0x0F; - final int calSampleLocation = (calTypeSampleLocation & 0xF0) >> 4; - final int calNextCalibrationTime = data.getIntValue(Data.FORMAT_UINT16, offset); - // offset += 2; - // final int calCalibrationDataRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset); - // offset += 2; - // final int calStatus = data.getIntValue(Data.FORMAT_UINT8, offset++); - - builder.append(" to:\n"); - builder.append("Glucose Concentration of Calibration: ").append(calConcentration).append(" mg/dL\n"); - builder.append("Time: ").append(calTime).append(" min\n"); - builder.append("Type: ").append(parseType(calType)).append("\n"); - builder.append("Sample Location: ").append(parseSampleLocation(calSampleLocation)).append("\n"); - builder.append("Next Calibration Time: ").append(parseNextCalibrationTime(calNextCalibrationTime)).append(" min\n"); // field ignored on Set - // builder.append("Data Record Number: ").append(calCalibrationDataRecordNumber).append("\n"); // field ignored on Set - // parseStatus(builder, calStatus); // field ignored on Set - break; - } - case OP_GET_GLUCOSE_CALIBRATION_VALUE: { - final int calibrationRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset); - builder.append(": ").append(parseRecordNumber(calibrationRecordNumber)); - break; - } - case OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE: { - final float calConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - offset += 2; - final int calTime = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - final int calTypeSampleLocation = data.getIntValue(Data.FORMAT_UINT8, offset++); - final int calType = calTypeSampleLocation & 0x0F; - final int calSampleLocation = (calTypeSampleLocation & 0xF0) >> 4; - final int calNextCalibrationTime = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - final int calCalibrationDataRecordNumber = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - final int calStatus = data.getIntValue(Data.FORMAT_UINT8, offset); - - builder.append(":\n"); - if (calCalibrationDataRecordNumber > 0) { - builder.append("Glucose Concentration of Calibration: ").append(calConcentration).append(" mg/dL\n"); - builder.append("Time: ").append(calTime).append(" min\n"); - builder.append("Type: ").append(parseType(calType)).append("\n"); - builder.append("Sample Location: ").append(parseSampleLocation(calSampleLocation)).append("\n"); - builder.append("Next Calibration Time: ").append(parseNextCalibrationTime(calNextCalibrationTime)).append("\n"); - builder.append("Data Record Number: ").append(calCalibrationDataRecordNumber); - parseStatus(builder, calStatus); - } else { - builder.append("No Calibration Data Stored"); - } - break; - } - case OP_SET_PATIENT_HIGH_ALERT_LEVEL: - case OP_SET_PATIENT_LOW_ALERT_LEVEL: - case OP_SET_HYPO_ALERT_LEVEL: - case OP_SET_HYPER_ALERT_LEVEL: { - final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - builder.append(" to: ").append(level).append(" mg/dL"); - break; - } - case OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE: - case OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE: - case OP_HYPO_ALERT_LEVEL_RESPONSE: - case OP_HYPER_ALERT_LEVEL_RESPONSE: { - final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - builder.append(": ").append(level).append(" mg/dL"); - break; - } - case OP_SET_RATE_OF_DECREASE_ALERT_LEVEL: - case OP_SET_RATE_OF_INCREASE_ALERT_LEVEL: { - final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - builder.append(" to: ").append(level).append(" mg/dL/min"); - break; - } - case OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE: - case OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE: { - final float level = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - builder.append(": ").append(level).append(" mg/dL/min"); - break; - } - case OP_CODE_RESPONSE_CODE: - final int requestOpCode = data.getIntValue(Data.FORMAT_UINT8, offset++); - final int responseCode = data.getIntValue(Data.FORMAT_UINT8, offset++); - builder.append(" to ").append(parseOpCode(requestOpCode)).append(": ").append(parseResponseCode(responseCode)); - break; - } - - return builder.toString(); - } - - private static String parseOpCode(final int code) { - switch (code) { - case OP_SET_CGM_COMMUNICATION_INTERVAL: - return "Set CGM Communication Interval"; - case OP_GET_CGM_COMMUNICATION_INTERVAL: - return "Get CGM Communication Interval"; - case OP_CGM_COMMUNICATION_INTERVAL_RESPONSE: - return "CGM Communication Interval"; - case OP_SET_GLUCOSE_CALIBRATION_VALUE: - return "Set CGM Calibration Value"; - case OP_GET_GLUCOSE_CALIBRATION_VALUE: - return "Get CGM Calibration Value"; - case OP_GLUCOSE_CALIBRATION_VALUE_RESPONSE: - return "CGM Calibration Value"; - case OP_SET_PATIENT_HIGH_ALERT_LEVEL: - return "Set Patient High Alert Level"; - case OP_GET_PATIENT_HIGH_ALERT_LEVEL: - return "Get Patient High Alert Level"; - case OP_PATIENT_HIGH_ALERT_LEVEL_RESPONSE: - return "Patient High Alert Level"; - case OP_SET_PATIENT_LOW_ALERT_LEVEL: - return "Set Patient Low Alert Level"; - case OP_GET_PATIENT_LOW_ALERT_LEVEL: - return "Get Patient Low Alert Level"; - case OP_PATIENT_LOW_ALERT_LEVEL_RESPONSE: - return "Patient Low Alert Level"; - case OP_SET_HYPO_ALERT_LEVEL: - return "Set Hypo Alert Level"; - case OP_GET_HYPO_ALERT_LEVEL: - return "Get Hypo Alert Level"; - case OP_HYPO_ALERT_LEVEL_RESPONSE: - return "Hypo Alert Level"; - case OP_SET_HYPER_ALERT_LEVEL: - return "Set Hyper Alert Level"; - case OP_GET_HYPER_ALERT_LEVEL: - return "Get Hyper Alert Level"; - case OP_HYPER_ALERT_LEVEL_RESPONSE: - return "Hyper Alert Level"; - case OP_SET_RATE_OF_DECREASE_ALERT_LEVEL: - return "Set Rate of Decrease Alert Level"; - case OP_GET_RATE_OF_DECREASE_ALERT_LEVEL: - return "Get Rate of Decrease Alert Level"; - case OP_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE: - return "Rate of Decrease Alert Level"; - case OP_SET_RATE_OF_INCREASE_ALERT_LEVEL: - return "Set Rate of Increase Alert Level"; - case OP_GET_RATE_OF_INCREASE_ALERT_LEVEL: - return "Get Rate of Increase Alert Level"; - case OP_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE: - return "Rate of Increase Alert Level"; - case OP_RESET_DEVICE_SPECIFIC_ALERT: - return "Reset Device Specific Alert"; - case OP_CODE_START_SESSION: - return "Start Session"; - case OP_CODE_STOP_SESSION: - return "Stop Session"; - case OP_CODE_RESPONSE_CODE: - return "Response"; - default: - return "Reserved for future use (" + code + ")"; - } - } - - private static String parseResponseCode(final int code) { - switch (code) { - case 1: return "Success"; - case 2: return "Op Code not supported"; - case 3: return "Invalid Operand"; - case 4: return "Procedure not completed"; - case 5: return "Parameter out of range"; - default: - return "Reserved for future use (" + code + ")"; - } - } - - private static String parseType(final int type) { - switch (type) { - case 1: return "Capillary Whole blood"; - case 2: return "Capillary Plasma"; - case 3: return "Capillary Whole blood"; - case 4: return "Venous Plasma"; - case 5: return "Arterial Whole blood"; - case 6: return "Arterial Plasma"; - case 7: return "Undetermined Whole blood"; - case 8: return "Undetermined Plasma"; - case 9: return "Interstitial Fluid (ISF)"; - case 10: return "Control Solution"; - default: return "Reserved for future use (" + type + ")"; - } - } - - private static String parseSampleLocation(final int location) { - switch (location) { - case 1: return "Finger"; - case 2: return "Alternate Site Test (AST)"; - case 3: return "Earlobe"; - case 4: return "Control solution"; - case 5: return "Subcutaneous tissue"; - case 15: return "Sample Location value not available"; - default: return "Reserved for future use (" + location + ")"; - } - } - - private static String parseNextCalibrationTime(final int time) { - if (time == 0) - return "Calibration Required Instantly"; - return time + " min"; - } - - private static String parseRecordNumber(final int time) { - if (time == 0xFFFF) - return "Last Calibration Data"; - return String.valueOf(time); - } - - private static void parseStatus(final StringBuilder builder, final int status) { - if (status == 0) - return; - builder.append("\nStatus:\n"); - if ((status & 1) > 0) - builder.append("- Calibration Data rejected"); - if ((status & 2) > 0) - builder.append("- Calibration Data out of range"); - if ((status & 4) > 0) - builder.append("- Calibration Process pending"); - if ((status & 0xF8) > 0) - builder.append("- Reserved for future use (").append(status).append(")"); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CSCMeasurementParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CSCMeasurementParser.java deleted file mode 100644 index a5a28cce..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/CSCMeasurementParser.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class CSCMeasurementParser { - private static final byte WHEEL_REV_DATA_PRESENT = 0x01; // 1 bit - private static final byte CRANK_REV_DATA_PRESENT = 0x02; // 1 bit - - public static String parse(final Data data) { - int offset = 0; - final int flags = data.getByte(offset); // 1 byte - offset += 1; - - final boolean wheelRevPresent = (flags & WHEEL_REV_DATA_PRESENT) > 0; - final boolean crankRevPreset = (flags & CRANK_REV_DATA_PRESENT) > 0; - - int wheelRevolutions = 0; - int lastWheelEventTime = 0; - if (wheelRevPresent) { - wheelRevolutions = data.getIntValue(Data.FORMAT_UINT32, offset); - offset += 4; - - lastWheelEventTime = data.getIntValue(Data.FORMAT_UINT16, offset); // 1/1024 s - offset += 2; - } - - int crankRevolutions = 0; - int lastCrankEventTime = 0; - if (crankRevPreset) { - crankRevolutions = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - - lastCrankEventTime = data.getIntValue(Data.FORMAT_UINT16, offset); - //offset += 2; - } - - final StringBuilder builder = new StringBuilder(); - if (wheelRevPresent) { - builder.append("Wheel rev: ").append(wheelRevolutions).append(",\n"); - builder.append("Last wheel event time: ").append(lastWheelEventTime).append(",\n"); - } - if (crankRevPreset) { - builder.append("Crank rev: ").append(crankRevolutions).append(",\n"); - builder.append("Last crank event time: ").append(lastCrankEventTime).append(",\n"); - } - if (!wheelRevPresent && !crankRevPreset) { - builder.append("No wheel or crank data"); - } - builder.setLength(builder.length() - 2); - return builder.toString(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/DateTimeParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/DateTimeParser.java deleted file mode 100644 index 5e3e79b0..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/DateTimeParser.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import java.util.Calendar; -import java.util.Locale; - -import no.nordicsemi.android.ble.common.callback.DateTimeDataCallback; -import no.nordicsemi.android.ble.data.Data; - -public class DateTimeParser { - /** - * Parses the date and time info. - * - * @param data - * @return time in human readable format - */ - public static String parse(final Data data) { - return parse(data, 0); - } - - /** - * Parses the date and time info. This data has 7 bytes - * - * @param data - * @param offset - * offset to start reading the time - * @return time in human readable format - */ - /* package */static String parse(final Data data, final int offset) { - final Calendar calendar = DateTimeDataCallback.readDateTime(data, offset); - return String.format(Locale.US, "%1$te %1$tb %1$tY, %1$tH:%1$tM:%1$tS", calendar); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementContextParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementContextParser.java deleted file mode 100644 index 8a814406..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementContextParser.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class GlucoseMeasurementContextParser { - private static final int UNIT_kg = 0; - private static final int UNIT_l = 1; - - public static String parse(final Data data) { - final StringBuilder builder = new StringBuilder(); - - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset); - offset += 1; - - final boolean carbohydratePresent = (flags & 0x01) > 0; - final boolean mealPresent = (flags & 0x02) > 0; - final boolean testerHealthPresent = (flags & 0x04) > 0; - final boolean exercisePresent = (flags & 0x08) > 0; - final boolean medicationPresent = (flags & 0x10) > 0; - final int medicationUnit = (flags & 0x20) > 0 ? UNIT_l : UNIT_kg; - final boolean hbA1cPresent = (flags & 0x40) > 0; - final boolean moreFlagsPresent = (flags & 0x80) > 0; - - final int sequenceNumber = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - - if (moreFlagsPresent) // not supported yet - offset += 1; - - builder.append("Sequence number: ").append(sequenceNumber); - - if (carbohydratePresent) { - final int carbohydrateId = data.getIntValue(Data.FORMAT_UINT8, offset); - final float carbohydrateUnits = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 1); - builder.append("\nCarbohydrate: ").append(getCarbohydrate(carbohydrateId)).append(" (").append(carbohydrateUnits).append(carbohydrateUnits == UNIT_kg ? "kg" : "l").append(")"); - offset += 3; - } - - if (mealPresent) { - final int meal = data.getIntValue(Data.FORMAT_UINT8, offset); - builder.append("\nMeal: ").append(getMeal(meal)); - offset += 1; - } - - if (testerHealthPresent) { - final int testerHealth = data.getIntValue(Data.FORMAT_UINT8, offset); - final int tester = (testerHealth & 0xF0) >> 4; - final int health = (testerHealth & 0x0F); - builder.append("\nTester: ").append(getTester(tester)); - builder.append("\nHealth: ").append(getHealth(health)); - offset += 1; - } - - if (exercisePresent) { - final int exerciseDuration = data.getIntValue(Data.FORMAT_UINT16, offset); - final int exerciseIntensity = data.getIntValue(Data.FORMAT_UINT8, offset + 2); - builder.append("\nExercise duration: ").append(exerciseDuration).append("s (intensity ").append(exerciseIntensity).append("%)"); - offset += 3; - } - - if (medicationPresent) { - final int medicationId = data.getIntValue(Data.FORMAT_UINT8, offset); - final float medicationQuantity = data.getFloatValue(Data.FORMAT_SFLOAT, offset + 1); - builder.append("\nMedication: ").append(getMedicationId(medicationId)).append(" (").append(medicationQuantity).append(medicationUnit == UNIT_kg ? "kg" : "l"); - offset += 3; - } - - if (hbA1cPresent) { - final float HbA1c = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - builder.append("\nHbA1c: ").append(HbA1c).append("%"); - } - return builder.toString(); - } - - private static String getCarbohydrate(final int id) { - switch (id) { - case 1: - return "Breakfast"; - case 2: - return "Lunch"; - case 3: - return "Dinner"; - case 4: - return "Snack"; - case 5: - return "Drink"; - case 6: - return "Supper"; - case 7: - return "Brunch"; - default: - return "Reserved for future use (" + id + ")"; - } - } - - private static String getMeal(final int id) { - switch (id) { - case 1: - return "Preprandial (before meal)"; - case 2: - return "Postprandial (after meal)"; - case 3: - return "Fasting"; - case 4: - return "Casual (snacks, drinks, etc.)"; - case 5: - return "Bedtime"; - default: - return "Reserved for future use (" + id + ")"; - } - } - - private static String getTester(final int id) { - switch (id) { - case 1: - return "Self"; - case 2: - return "Health Care Professional"; - case 3: - return "Lab test"; - case 4: - return "Casual (snacks, drinks, etc.)"; - case 15: - return "Tester value not available"; - default: - return "Reserved for future use (" + id + ")"; - } - } - - private static String getHealth(final int id) { - switch (id) { - case 1: - return "Minor health issues"; - case 2: - return "Major health issues"; - case 3: - return "During menses"; - case 4: - return "Under stress"; - case 5: - return "No health issues"; - case 15: - return "Health value not available"; - default: - return "Reserved for future use (" + id + ")"; - } - } - - private static String getMedicationId(final int id) { - switch (id) { - case 1: - return "Rapid acting insulin"; - case 2: - return "Short acting insulin"; - case 3: - return "Intermediate acting insulin"; - case 4: - return "Long acting insulin"; - case 5: - return "Pre-mixed insulin"; - default: - return "Reserved for future use (" + id + ")"; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementParser.java deleted file mode 100644 index 18d59b44..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/GlucoseMeasurementParser.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class GlucoseMeasurementParser { - private static final int UNIT_kgpl = 0; - private static final int UNIT_molpl = 1; - - private static final int STATUS_DEVICE_BATTERY_LOW = 0x0001; - private static final int STATUS_SENSOR_MALFUNCTION = 0x0002; - private static final int STATUS_SAMPLE_SIZE_FOR_BLOOD_OR_CONTROL_SOLUTION_INSUFFICIENT = 0x0004; - private static final int STATUS_STRIP_INSERTION_ERROR = 0x0008; - private static final int STATUS_STRIP_TYPE_INCORRECT_FOR_DEVICE = 0x0010; - private static final int STATUS_SENSOR_RESULT_TOO_HIGH = 0x0020; - private static final int STATUS_SENSOR_RESULT_TOO_LOW = 0x0040; - private static final int STATUS_SENSOR_TEMPERATURE_TOO_HIGH = 0x0080; - private static final int STATUS_SENSOR_TEMPERATURE_TOO_LOW = 0x0100; - private static final int STATUS_SENSOR_READ_INTERRUPTED = 0x0200; - private static final int STATUS_GENERAL_DEVICE_FAULT = 0x0400; - private static final int STATUS_TIME_FAULT = 0x0800; - - public static String parse(final Data data) { - final StringBuilder builder = new StringBuilder(); - - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset); - offset += 1; - - final boolean timeOffsetPresent = (flags & 0x01) > 0; - final boolean typeAndLocationPresent = (flags & 0x02) > 0; - final int concentrationUnit = (flags & 0x04) > 0 ? UNIT_molpl : UNIT_kgpl; - final boolean sensorStatusAnnunciationPresent = (flags & 0x08) > 0; - final boolean contextInfoFollows = (flags & 0x10) > 0; - - // create and fill the new record - final int sequenceNumber = data.getIntValue(Data.FORMAT_UINT16, offset); - builder.append("Sequence Number: ").append(sequenceNumber); - offset += 2; - - builder.append("\nBase Time: ").append(DateTimeParser.parse(data, offset)); - offset += 7; - - if (timeOffsetPresent) { - // time offset is ignored in the current release - final int timeOffset = data.getIntValue(Data.FORMAT_SINT16, offset); - builder.append("\nTime Offset: ").append(timeOffset).append(" min"); - offset += 2; - } - - if (typeAndLocationPresent) { - final float glucoseConcentration = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - final int typeAndLocation = data.getIntValue(Data.FORMAT_UINT8, offset + 2); - final int type = (typeAndLocation & 0xF0) >> 4; // TODO this way or around? - final int sampleLocation = (typeAndLocation & 0x0F); - builder.append("\nGlucose Concentration: ").append(glucoseConcentration).append(concentrationUnit == UNIT_kgpl ? " kg/l" : " mol/l"); - builder.append("\nSample Type: ").append(getType(type)); - builder.append("\nSample Location: ").append(getLocation(sampleLocation)); - offset += 3; - } - - if (sensorStatusAnnunciationPresent) { - final int status = data.getIntValue(Data.FORMAT_UINT16, offset); - builder.append("Status:\n").append(getStatusAnnunciation(status)); - } - - builder.append("\nContext information follows: ").append(contextInfoFollows); - return builder.toString(); - } - - private static String getType(final int type) { - switch (type) { - case 1: - return "Capillary Whole blood"; - case 2: - return "Capillary Plasma"; - case 3: - return "Venous Whole blood"; - case 4: - return "Venous Plasma"; - case 5: - return "Arterial Whole blood"; - case 6: - return "Arterial Plasma"; - case 7: - return "Undetermined Whole blood"; - case 8: - return "Undetermined Plasma"; - case 9: - return "Interstitial Fluid (ISF)"; - case 10: - return "Control Solution"; - default: - return "Reserved for future use (" + type + ")"; - } - } - - private static String getLocation(final int location) { - switch (location) { - case 1: - return "Finger"; - case 2: - return "Alternate Site Test (AST)"; - case 3: - return "Earlobe"; - case 4: - return "Control solution"; - case 15: - return "Value not available"; - default: - return "Reserved for future use (" + location + ")"; - } - } - - private static String getStatusAnnunciation(final int status) { - final StringBuilder builder = new StringBuilder(); - if ((status & STATUS_DEVICE_BATTERY_LOW) > 0) - builder.append("\nDevice battery low at time of measurement"); - if ((status & STATUS_SENSOR_MALFUNCTION) > 0) - builder.append("\nSensor malfunction or faulting at time of measurement"); - if ((status & STATUS_SAMPLE_SIZE_FOR_BLOOD_OR_CONTROL_SOLUTION_INSUFFICIENT) > 0) - builder.append("\nSample size for blood or control solution insufficient at time of measurement"); - if ((status & STATUS_STRIP_INSERTION_ERROR) > 0) - builder.append("\nStrip insertion error"); - if ((status & STATUS_STRIP_TYPE_INCORRECT_FOR_DEVICE) > 0) - builder.append("\nStrip type incorrect for device"); - if ((status & STATUS_SENSOR_RESULT_TOO_HIGH) > 0) - builder.append("\nSensor result higher than the device can process"); - if ((status & STATUS_SENSOR_RESULT_TOO_LOW) > 0) - builder.append("\nSensor result lower than the device can process"); - if ((status & STATUS_SENSOR_TEMPERATURE_TOO_HIGH) > 0) - builder.append("\nSensor temperature too high for valid test/result at time of measurement"); - if ((status & STATUS_SENSOR_TEMPERATURE_TOO_LOW) > 0) - builder.append("\nSensor temperature too low for valid test/result at time of measurement"); - if ((status & STATUS_SENSOR_READ_INTERRUPTED) > 0) - builder.append("\nSensor read interrupted because strip was pulled too soon at time of measurement"); - if ((status & STATUS_GENERAL_DEVICE_FAULT) > 0) - builder.append("\nGeneral device fault has occurred in the sensor"); - if ((status & STATUS_TIME_FAULT) > 0) - builder.append("\nTime fault has occurred in the sensor and time may be inaccurate"); - return builder.toString(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/HeartRateMeasurementParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/HeartRateMeasurementParser.java deleted file mode 100644 index 96220727..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/HeartRateMeasurementParser.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class HeartRateMeasurementParser { - private static final byte HEART_RATE_VALUE_FORMAT = 0x01; // 1 bit - private static final byte SENSOR_CONTACT_STATUS = 0x06; // 2 bits - private static final byte ENERGY_EXPANDED_STATUS = 0x08; // 1 bit - private static final byte RR_INTERVAL = 0x10; // 1 bit - - public static String parse(final Data data) { - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); - - /* - * false Heart Rate Value Format is set to UINT8. Units: beats per minute (bpm) - * true Heart Rate Value Format is set to UINT16. Units: beats per minute (bpm) - */ - final boolean value16bit = (flags & HEART_RATE_VALUE_FORMAT) > 0; - - /* - * 0 Sensor Contact feature is not supported in the current connection - * 1 Sensor Contact feature is not supported in the current connection - * 2 Sensor Contact feature is supported, but contact is not detected - * 3 Sensor Contact feature is supported and contact is detected - */ - final int sensorContactStatus = (flags & SENSOR_CONTACT_STATUS) >> 1; - - /* - * false Energy Expended field is not present - * true Energy Expended field is present. Units: kilo Joules - */ - final boolean energyExpandedStatus = (flags & ENERGY_EXPANDED_STATUS) > 0; - - /* - * false RR-Interval values are not present. - * true One or more RR-Interval values are present. Units: 1/1024 seconds - */ - final boolean rrIntervalStatus = (flags & RR_INTERVAL) > 0; - - // heart rate value is 8 or 16 bit long - int heartRateValue = data.getIntValue(value16bit ? Data.FORMAT_UINT16 : Data.FORMAT_UINT8, offset++); // bits per minute - if (value16bit) - offset++; - - // energy expanded value is present if a flag was set - int energyExpanded = -1; - if (energyExpandedStatus) - energyExpanded = data.getIntValue(Data.FORMAT_UINT16, offset); - offset += 2; - - // RR-interval is set when a flag is set - final List rrIntervals = new ArrayList<>(); - if (rrIntervalStatus) { - for (int o = offset; o < data.getValue().length; o += 2) { - final int units = data.getIntValue(Data.FORMAT_UINT16, o); - rrIntervals.add(units * 1000.0f / 1024.0f); // RR interval is in [1/1024s] - } - } - - final StringBuilder builder = new StringBuilder(); - builder.append("Heart Rate Measurement: ").append(heartRateValue).append(" bpm"); - switch (sensorContactStatus) { - case 0: - case 1: - builder.append(",\nSensor Contact Not Supported"); - break; - case 2: - builder.append(",\nContact is NOT Detected"); - break; - case 3: - builder.append(",\nContact is Detected"); - break; - } - if (energyExpandedStatus) - builder.append(",\nEnergy Expanded: ").append(energyExpanded).append(" kJ"); - if (rrIntervalStatus) { - builder.append(",\nRR Interval: "); - for (final Float interval : rrIntervals) - builder.append(String.format(Locale.US, "%.02f ms, ", interval)); - builder.setLength(builder.length() - 2); // remove the ", " at the end - } - return builder.toString(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/IntermediateCuffPressureParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/IntermediateCuffPressureParser.java deleted file mode 100644 index 632dc5e2..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/IntermediateCuffPressureParser.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class IntermediateCuffPressureParser { - public static String parse(final Data data) { - final StringBuilder builder = new StringBuilder(); - - // first byte - flags - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); - - final int unitType = flags & 0x01; - final boolean timestampPresent = (flags & 0x02) > 0; - final boolean pulseRatePresent = (flags & 0x04) > 0; - final boolean userIdPresent = (flags & 0x08) > 0; - final boolean statusPresent = (flags & 0x10) > 0; - - // following bytes - pressure - final float pressure = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - final String unit = unitType == 0 ? " mmHg" : " kPa"; - offset += 6; - builder.append("Cuff pressure: ").append(pressure).append(unit); - - // parse timestamp if present - if (timestampPresent) { - builder.append("Timestamp: ").append(DateTimeParser.parse(data, offset)); - offset += 7; - } - - // parse pulse rate if present - if (pulseRatePresent) { - final float pulseRate = data.getFloatValue(Data.FORMAT_SFLOAT, offset); - offset += 2; - builder.append("\nPulse: ").append(pulseRate).append(" bpm"); - } - - if (userIdPresent) { - final int userId = data.getIntValue(Data.FORMAT_UINT8, offset); - offset += 1; - builder.append("\nUser ID: ").append(userId); - } - - if (statusPresent) { - final int status = data.getIntValue(Data.FORMAT_UINT16, offset); - // offset += 2; - if ((status & 0x0001) > 0) - builder.append("\nBody movement detected"); - if ((status & 0x0002) > 0) - builder.append("\nCuff too lose"); - if ((status & 0x0004) > 0) - builder.append("\nIrregular pulse detected"); - if ((status & 0x0018) == 0x0008) - builder.append("\nPulse rate exceeds upper limit"); - if ((status & 0x0018) == 0x0010) - builder.append("\nPulse rate is less than lower limit"); - if ((status & 0x0018) == 0x0018) - builder.append("\nPulse rate range: Reserved for future use "); - if ((status & 0x0020) > 0) - builder.append("\nImproper measurement position"); - } - - return builder.toString(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RSCMeasurementParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RSCMeasurementParser.java deleted file mode 100644 index 6d196229..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RSCMeasurementParser.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import java.util.Locale; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class RSCMeasurementParser { - private static final byte INSTANTANEOUS_STRIDE_LENGTH_PRESENT = 0x01; // 1 bit - private static final byte TOTAL_DISTANCE_PRESENT = 0x02; // 1 bit - private static final byte WALKING_OR_RUNNING_STATUS_BITS = 0x04; // 1 bit - - public static String parse(final Data data) { - int offset = 0; - final int flags = data.getValue()[offset]; // 1 byte - offset += 1; - - final boolean islmPresent = (flags & INSTANTANEOUS_STRIDE_LENGTH_PRESENT) > 0; - final boolean tdPreset = (flags & TOTAL_DISTANCE_PRESENT) > 0; - final boolean running = (flags & WALKING_OR_RUNNING_STATUS_BITS) > 0; - final boolean walking = !running; - - final float instantaneousSpeed = (float) data.getIntValue(Data.FORMAT_UINT16, offset) / 256.0f; // 1/256 m/s - offset += 2; - - final int instantaneousCadence = data.getIntValue(Data.FORMAT_UINT8, offset); - offset += 1; - - float instantaneousStrideLength = 0; - if (islmPresent) { - instantaneousStrideLength = (float) data.getIntValue(Data.FORMAT_UINT16, offset) / 100.0f; // 1/100 m - offset += 2; - } - - float totalDistance = 0; - if (tdPreset) { - totalDistance = (float) data.getIntValue(Data.FORMAT_UINT32, offset) / 10.0f; - // offset += 4; - } - - final StringBuilder builder = new StringBuilder(); - builder.append(String.format(Locale.US, "Speed: %.2f m/s, Cadence: %d RPM,\n", instantaneousSpeed, instantaneousCadence)); - if (islmPresent) - builder.append(String.format(Locale.US, "Instantaneous Stride Length: %.2f m,\n", instantaneousStrideLength)); - if (tdPreset) - builder.append(String.format(Locale.US, "Total Distance: %.1f m,\n", totalDistance)); - if (walking) - builder.append("Status: WALKING"); - else - builder.append("Status: RUNNING"); - return builder.toString(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RecordAccessControlPointParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RecordAccessControlPointParser.java deleted file mode 100644 index 0ef5954c..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/RecordAccessControlPointParser.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class RecordAccessControlPointParser { - private final static int OP_CODE_REPORT_STORED_RECORDS = 1; - private final static int OP_CODE_DELETE_STORED_RECORDS = 2; - private final static int OP_CODE_ABORT_OPERATION = 3; - private final static int OP_CODE_REPORT_NUMBER_OF_RECORDS = 4; - private final static int OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5; - private final static int OP_CODE_RESPONSE_CODE = 6; - - private final static int OPERATOR_NULL = 0; - private final static int OPERATOR_ALL_RECORDS = 1; - private final static int OPERATOR_LESS_THEN_OR_EQUAL = 2; - private final static int OPERATOR_GREATER_THEN_OR_EQUAL = 3; - private final static int OPERATOR_WITHING_RANGE = 4; - private final static int OPERATOR_FIRST_RECORD = 5; - private final static int OPERATOR_LAST_RECORD = 6; - - private final static int RESPONSE_SUCCESS = 1; - private final static int RESPONSE_OP_CODE_NOT_SUPPORTED = 2; - private final static int RESPONSE_INVALID_OPERATOR = 3; - private final static int RESPONSE_OPERATOR_NOT_SUPPORTED = 4; - private final static int RESPONSE_INVALID_OPERAND = 5; - private final static int RESPONSE_NO_RECORDS_FOUND = 6; - private final static int RESPONSE_ABORT_UNSUCCESSFUL = 7; - private final static int RESPONSE_PROCEDURE_NOT_COMPLETED = 8; - private final static int RESPONSE_OPERAND_NOT_SUPPORTED = 9; - - public static String parse(final Data data) { - final StringBuilder builder = new StringBuilder(); - final int opCode = data.getIntValue(Data.FORMAT_UINT8, 0); - final int operator = data.getIntValue(Data.FORMAT_UINT8, 1); - - switch (opCode) { - case OP_CODE_REPORT_STORED_RECORDS: - case OP_CODE_DELETE_STORED_RECORDS: - case OP_CODE_ABORT_OPERATION: - case OP_CODE_REPORT_NUMBER_OF_RECORDS: - builder.append(getOpCode(opCode)).append("\n"); - break; - case OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE: { - builder.append(getOpCode(opCode)).append(": "); - final int value = data.getIntValue(Data.FORMAT_UINT16, 2); - builder.append(value).append("\n"); - break; - } - case OP_CODE_RESPONSE_CODE: { - builder.append(getOpCode(opCode)).append(" for "); - final int targetOpCode = data.getIntValue(Data.FORMAT_UINT8, 2); - builder.append(getOpCode(targetOpCode)).append(": "); - final int status = data.getIntValue(Data.FORMAT_UINT8, 3); - builder.append(getStatus(status)).append("\n"); - break; - } - } - - switch (operator) { - case OPERATOR_ALL_RECORDS: - case OPERATOR_FIRST_RECORD: - case OPERATOR_LAST_RECORD: - builder.append("Operator: ").append(getOperator(operator)).append("\n"); - break; - case OPERATOR_GREATER_THEN_OR_EQUAL: - case OPERATOR_LESS_THEN_OR_EQUAL: { - final int filter = data.getIntValue(Data.FORMAT_UINT8, 2); - final int value = data.getIntValue(Data.FORMAT_UINT16, 3); - builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value).append(" (filter: ").append(filter).append(")\n"); - break; - } - case OPERATOR_WITHING_RANGE: { - final int filter = data.getIntValue(Data.FORMAT_UINT8, 2); - final int value1 = data.getIntValue(Data.FORMAT_UINT16, 3); - final int value2 = data.getIntValue(Data.FORMAT_UINT16, 5); - builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value1).append("-").append(value2).append(" (filter: ").append(filter).append(")\n"); - break; - } - } - if (builder.length() > 0) - builder.setLength(builder.length() - 1); - - return builder.toString(); - } - - private static String getOpCode(final int opCode) { - switch (opCode) { - case OP_CODE_REPORT_STORED_RECORDS: - return "Report stored records"; - case OP_CODE_DELETE_STORED_RECORDS: - return "Delete stored records"; - case OP_CODE_ABORT_OPERATION: - return "Abort operation"; - case OP_CODE_REPORT_NUMBER_OF_RECORDS: - return "Report number of stored records"; - case OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE: - return "Number of stored records response"; - case OP_CODE_RESPONSE_CODE: - return "Response Code"; - default: - return "Reserved for future use"; - } - } - - private static String getOperator(final int operator) { - switch (operator) { - case OPERATOR_NULL: - return "Null"; - case OPERATOR_ALL_RECORDS: - return "All records"; - case OPERATOR_LESS_THEN_OR_EQUAL: - return "Less than or equal to"; - case OPERATOR_GREATER_THEN_OR_EQUAL: - return "Greater than or equal to"; - case OPERATOR_WITHING_RANGE: - return "Within range of"; - case OPERATOR_FIRST_RECORD: - return "First record(i.e. oldest record)"; - case OPERATOR_LAST_RECORD: - return "Last record (i.e. most recent record)"; - default: - return "Reserved for future use"; - } - } - - private static String getStatus(final int status) { - switch (status) { - case RESPONSE_SUCCESS: - return "Success"; - case RESPONSE_OP_CODE_NOT_SUPPORTED: - return "Operation not supported"; - case RESPONSE_INVALID_OPERATOR: - return "Invalid operator"; - case RESPONSE_OPERATOR_NOT_SUPPORTED: - return "Operator not supported"; - case RESPONSE_INVALID_OPERAND: - return "Invalid operand"; - case RESPONSE_NO_RECORDS_FOUND: - return "No records found"; - case RESPONSE_ABORT_UNSUCCESSFUL: - return "Abort unsuccessful"; - case RESPONSE_PROCEDURE_NOT_COMPLETED: - return "Procedure not completed"; - case RESPONSE_OPERAND_NOT_SUPPORTED: - return "Operand not supported"; - default: - return "Reserved for future use"; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureMeasurementParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureMeasurementParser.java deleted file mode 100644 index cdefad70..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureMeasurementParser.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import java.util.Locale; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class TemperatureMeasurementParser { - private static final byte TEMPERATURE_UNIT_FLAG = 0x01; // 1 bit - private static final byte TIMESTAMP_FLAG = 0x02; // 1 bits - private static final byte TEMPERATURE_TYPE_FLAG = 0x04; // 1 bit - - public static String parse(final Data data) { - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); - - /* - * false Temperature is in Celsius degrees - * true Temperature is in Fahrenheit degrees - */ - final boolean fahrenheit = (flags & TEMPERATURE_UNIT_FLAG) > 0; - - /* - * false No Timestamp in the packet - * true There is a timestamp information - */ - final boolean timestampIncluded = (flags & TIMESTAMP_FLAG) > 0; - - /* - * false Temperature type is not included - * true Temperature type included in the packet - */ - final boolean temperatureTypeIncluded = (flags & TEMPERATURE_TYPE_FLAG) > 0; - - final float tempValue = data.getFloatValue(Data.FORMAT_FLOAT, offset); - offset += 4; - - String dateTime = null; - if (timestampIncluded) { - dateTime = DateTimeParser.parse(data, offset); - offset += 7; - } - - String type = null; - if (temperatureTypeIncluded) { - type = TemperatureTypeParser.parse(data, offset); - // offset++; - } - - final StringBuilder builder = new StringBuilder(); - builder.append(String.format(Locale.US, "%.02f", tempValue)); - - if (fahrenheit) - builder.append("°F"); - else - builder.append("°C"); - - if (timestampIncluded) - builder.append("\nTime: ").append(dateTime); - if (temperatureTypeIncluded) - builder.append("\nType: ").append(type); - return builder.toString(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureTypeParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureTypeParser.java deleted file mode 100644 index 10d755ed..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemperatureTypeParser.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -@SuppressWarnings("ConstantConditions") -public class TemperatureTypeParser { - - public static String parse(final Data data) { - return parse(data, 0); - } - - /* package */static String parse(final Data data, final int offset) { - final int type = data.getValue()[offset]; - - switch (type) { - case 1: - return "Armpit"; - case 2: - return "Body (general)"; - case 3: - return "Ear (usually ear lobe)"; - case 4: - return "Finger"; - case 5: - return "Gastro-intestinal Tract"; - case 6: - return "Mouth"; - case 7: - return "Rectum"; - case 8: - return "Toe"; - case 9: - return "Tympanum (ear drum)"; - default: - return "Unknown"; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemplateParser.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemplateParser.java deleted file mode 100644 index ed0490a4..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/parser/TemplateParser.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2015, 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.parser; - -import no.nordicsemi.android.ble.data.Data; - -// TODO this method may be used for developing purposes to log the data from your device using the nRF Logger application. - -@SuppressWarnings("ConstantConditions") -public class TemplateParser { - // TODO add some flags, if needed - private static final byte HEART_RATE_VALUE_FORMAT = 0x01; // 1 bit - - /** - * This method converts the value of the characteristic to the String. The String is then logged in the nRF logger log session - * @param data the characteristic data to be parsed - * @return human readable value of the characteristic - */ - @SuppressWarnings("UnusedAssignment") - public static String parse(final Data data) { - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); - - /* - * In the template we are using the HRM values as an example. - * false Heart Rate Value Format is set to UINT8. Units: beats per minute (bpm) - * true Heart Rate Value Format is set to UINT16. Units: beats per minute (bpm) - */ - final boolean value16bit = (flags & HEART_RATE_VALUE_FORMAT) > 0; - - // heart rate value is 8 or 16 bit long - int value = data.getIntValue(value16bit ? Data.FORMAT_UINT16 : Data.FORMAT_UINT8, offset++); // bits per minute - if (value16bit) - offset++; - - // TODO parse more data - - return "Template Measurement: " + value + " bpm"; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java deleted file mode 100644 index 79518e15..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileActivity.java +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright (c) 2015, 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.profile; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.UUID; - -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.LocalLogSession; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.AppHelpFragment; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; - -@SuppressWarnings("unused") -public abstract class BleProfileActivity extends AppCompatActivity implements BleManagerCallbacks, ScannerFragment.OnDeviceSelectedListener { - private static final String TAG = "BaseProfileActivity"; - - private static final String SIS_CONNECTION_STATUS = "connection_status"; - private static final String SIS_DEVICE_NAME = "device_name"; - protected static final int REQUEST_ENABLE_BT = 2; - - private LoggableBleManager bleManager; - - private TextView deviceNameView; - private Button connectButton; - private ILogSession logSession; - - private boolean deviceConnected = false; - private String deviceName; - - @Override - protected final void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - ensureBLESupported(); - if (!isBLEEnabled()) { - showBLEDialog(); - } - - /* - * We use the managers using a singleton pattern. It's not recommended for the Android, because the singleton instance remains after Activity has been - * destroyed but it's simple and is used only for this demo purpose. In final application Managers should be created as a non-static objects in - * Services. The Service should implement ManagerCallbacks interface. The application Activity may communicate with such Service using binding, - * broadcast listeners, local broadcast listeners (see support.v4 library), or messages. See the Proximity profile for Service approach. - */ - bleManager = initializeManager(); - - // In onInitialize method a final class may register local broadcast receivers that will listen for events from the service - onInitialize(savedInstanceState); - // The onCreateView class should... create the view - onCreateView(savedInstanceState); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - - // Common nRF Toolbox view references are obtained here - setUpView(); - // View is ready to be used - onViewCreated(savedInstanceState); - } - - /** - * You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created. - */ - protected void onInitialize(@Nullable final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}. - * Use to obtain references to views. Connect/Disconnect button and the device name view are manager automatically. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - protected abstract void onCreateView(@Nullable final Bundle savedInstanceState); - - /** - * Called after the view has been created. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - protected void onViewCreated(@Nullable final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called after the view and the toolbar has been created. - */ - protected final void setUpView() { - // set GUI - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - connectButton = findViewById(R.id.action_connect); - deviceNameView = findViewById(R.id.device_name); - } - - @Override - public void onBackPressed() { - bleManager.disconnect().enqueue(); - super.onBackPressed(); - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(SIS_CONNECTION_STATUS, deviceConnected); - outState.putString(SIS_DEVICE_NAME, deviceName); - } - - @Override - protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - deviceConnected = savedInstanceState.getBoolean(SIS_CONNECTION_STATUS); - deviceName = savedInstanceState.getString(SIS_DEVICE_NAME); - - if (deviceConnected) { - connectButton.setText(R.string.action_disconnect); - } else { - connectButton.setText(R.string.action_connect); - } - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.help, menu); - return true; - } - - /** - * Use this method to handle menu actions other than home and about. - * - * @param itemId the menu item id - * @return true if action has been handled - */ - protected boolean onOptionsItemSelected(final int itemId) { - // Overwrite when using menu other than R.menu.help - return false; - } - - @Override - public boolean onOptionsItemSelected(@NonNull final MenuItem item) { - final int id = item.getItemId(); - switch (id) { - case android.R.id.home: - onBackPressed(); - break; - case R.id.action_about: - final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId()); - fragment.show(getSupportFragmentManager(), "help_fragment"); - break; - default: - return onOptionsItemSelected(id); - } - return true; - } - - /** - * Called when user press CONNECT or DISCONNECT button. See layout files -> onClick attribute. - */ - public void onConnectClicked(final View view) { - if (isBLEEnabled()) { - if (!deviceConnected) { - setDefaultUI(); - showDeviceScanningDialog(getFilterUUID()); - } else { - bleManager.disconnect().enqueue(); - } - } else { - showBLEDialog(); - } - } - - /** - * Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used. - * - * @return the title resource id - */ - protected int getLoggerProfileTitle() { - return 0; - } - - /** - * This method may return the local log content provider authority if local log sessions are supported. - * - * @return local log session content provider URI - */ - protected Uri getLocalAuthorityLogger() { - return null; - } - - /** - * This method returns whether autoConnect option should be used. - * - * @return true to use autoConnect feature, false (default) otherwise. - */ - protected boolean shouldAutoConnect() { - return false; - } - - @Override - public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) { - final int titleId = getLoggerProfileTitle(); - if (titleId > 0) { - logSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name); - // If nRF Logger is not installed we may want to use local logger - if (logSession == null && getLocalAuthorityLogger() != null) { - logSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name); - } - } - deviceName = name; - bleManager.setLogger(logSession); - bleManager.connect(device) - .useAutoConnect(shouldAutoConnect()) - .retry(3, 100) - .enqueue(); - } - - @Override - public void onDialogCanceled() { - // do nothing - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - runOnUiThread(() -> { - deviceNameView.setText(deviceName != null ? deviceName : getString(R.string.not_available)); - connectButton.setText(R.string.action_connecting); - }); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - deviceConnected = true; - runOnUiThread(() -> connectButton.setText(R.string.action_disconnect)); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - runOnUiThread(() -> connectButton.setText(R.string.action_disconnecting)); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - deviceConnected = false; - bleManager.close(); - runOnUiThread(() -> { - connectButton.setText(R.string.action_connect); - deviceNameView.setText(getDefaultDeviceName()); - }); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - deviceConnected = false; - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, boolean optionalServicesFound) { - // this may notify user or show some views - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - showToast(R.string.bonding); - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - showToast(R.string.bonded); - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - showToast(R.string.bonding_failed); - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode); - showToast(message + " (" + errorCode + ")"); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - showToast(R.string.not_supported); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message a message to be shown - */ - protected void showToast(final String message) { - runOnUiThread(() -> Toast.makeText(BleProfileActivity.this, message, Toast.LENGTH_SHORT).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - runOnUiThread(() -> Toast.makeText(BleProfileActivity.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns true if the device is connected. Services may not have been discovered yet. - */ - protected boolean isDeviceConnected() { - return deviceConnected; - } - - /** - * Returns the name of the device that the phone is currently connected to or was connected last time - */ - protected String getDeviceName() { - return deviceName; - } - - /** - * Initializes the Bluetooth Low Energy manager. A manager is used to communicate with profile's services. - * - * @return the manager that was created - */ - protected abstract LoggableBleManager initializeManager(); - - /** - * Restores the default UI before reconnecting - */ - protected abstract void setDefaultUI(); - - /** - * Returns the default device name resource id. The real device name is obtained when connecting to the device. This one is used when device has - * disconnected. - * - * @return the default device name resource id - */ - protected abstract int getDefaultDeviceName(); - - /** - * Returns the string resource id that will be shown in About box - * - * @return the about resource id - */ - protected abstract int getAboutTextId(); - - /** - * The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also: - * {@link #isChangingConfigurations()}. - * - * @return the required UUID or null - */ - protected abstract UUID getFilterUUID(); - - /** - * Shows the scanner fragment. - * - * @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their - * services - * @see #getFilterUUID() - */ - private void showDeviceScanningDialog(final UUID filter) { - runOnUiThread(() -> { - final ScannerFragment dialog = ScannerFragment.getInstance(filter); - dialog.show(getSupportFragmentManager(), "scan_fragment"); - }); - } - - /** - * Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT. - * - * @return the logger session or null - */ - protected ILogSession getLogSession() { - return logSession; - } - - private void ensureBLESupported() { - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { - Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show(); - finish(); - } - } - - protected boolean isBLEEnabled() { - final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); - final BluetoothAdapter adapter = bluetoothManager.getAdapter(); - return adapter != null && adapter.isEnabled(); - } - - protected void showBLEDialog() { - final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult(enableIntent, REQUEST_ENABLE_BT); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java deleted file mode 100644 index b3e0e842..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileExpandableListActivity.java +++ /dev/null @@ -1,443 +0,0 @@ -/* - * Copyright (c) 2015, 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.profile; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.UUID; - -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.LocalLogSession; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.AppHelpFragment; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.app.ExpandableListActivity; -import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; - -@SuppressWarnings("unused") -public abstract class BleProfileExpandableListActivity extends ExpandableListActivity implements BleManagerCallbacks, ScannerFragment.OnDeviceSelectedListener { - private static final String TAG = "BaseProfileActivity"; - - private static final String SIS_CONNECTION_STATUS = "connection_status"; - private static final String SIS_DEVICE_NAME = "device_name"; - protected static final int REQUEST_ENABLE_BT = 2; - - private LoggableBleManager bleManager; - - private TextView deviceNameView; - private Button connectButton; - private ILogSession logSession; - - private boolean deviceConnected = false; - private String deviceName; - - @Override - protected final void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - ensureBLESupported(); - if (!isBLEEnabled()) { - showBLEDialog(); - } - - /* - * We use the managers using a singleton pattern. It's not recommended for the Android, because the singleton instance remains after Activity has been - * destroyed but it's simple and is used only for this demo purpose. In final application Managers should be created as a non-static objects in - * Services. The Service should implement ManagerCallbacks interface. The application Activity may communicate with such Service using binding, - * broadcast listeners, local broadcast listeners (see support.v4 library), or messages. See the Proximity profile for Service approach. - */ - bleManager = initializeManager(); - - // In onInitialize method a final class may register local broadcast receivers that will listen for events from the service - onInitialize(savedInstanceState); - // The onCreateView class should... create the view - onCreateView(savedInstanceState); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - - // Common nRF Toolbox view references are obtained here - setUpView(); - // View is ready to be used - onViewCreated(savedInstanceState); - } - - /** - * You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created. - */ - @SuppressWarnings("unused") - protected void onInitialize(final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}. - * Use to obtain references to views. Connect/Disconnect button and the device name view are manager automatically. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - protected abstract void onCreateView(final Bundle savedInstanceState); - - /** - * Called after the view has been created. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - @SuppressWarnings("unused") - protected void onViewCreated(final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called after the view and the toolbar has been created. - */ - protected final void setUpView() { - // set GUI - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - connectButton = findViewById(R.id.action_connect); - deviceNameView = findViewById(R.id.device_name); - } - - @Override - public void onBackPressed() { - bleManager.disconnect().enqueue(); - super.onBackPressed(); - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(SIS_CONNECTION_STATUS, deviceConnected); - outState.putString(SIS_DEVICE_NAME, deviceName); - } - - @Override - protected void onRestoreInstanceState(final @NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - deviceConnected = savedInstanceState.getBoolean(SIS_CONNECTION_STATUS); - deviceName = savedInstanceState.getString(SIS_DEVICE_NAME); - - if (deviceConnected) { - connectButton.setText(R.string.action_disconnect); - } else { - connectButton.setText(R.string.action_connect); - } - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.help, menu); - return true; - } - - /** - * Use this method to handle menu actions other than home and about. - * - * @param itemId the menu item id - * @return true if action has been handled - */ - @SuppressWarnings("unused") - protected boolean onOptionsItemSelected(final int itemId) { - // Overwrite when using menu other than R.menu.help - return false; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - final int id = item.getItemId(); - switch (id) { - case android.R.id.home: - onBackPressed(); - break; - case R.id.action_about: - final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId()); - fragment.show(getSupportFragmentManager(), "help_fragment"); - break; - default: - return onOptionsItemSelected(id); - } - return true; - } - - /** - * Called when user press CONNECT or DISCONNECT button. See layout files -> onClick attribute. - */ - public void onConnectClicked(final View view) { - if (isBLEEnabled()) { - if (!deviceConnected) { - setDefaultUI(); - showDeviceScanningDialog(getFilterUUID()); - } else { - bleManager.disconnect().enqueue(); - } - } else { - showBLEDialog(); - } - } - - /** - * Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used. - * - * @return the title resource id - */ - protected int getLoggerProfileTitle() { - return 0; - } - - /** - * This method may return the local log content provider authority if local log sessions are supported. - * - * @return local log session content provider URI - */ - protected Uri getLocalAuthorityLogger() { - return null; - } - - /** - * This method returns whether autoConnect option should be used. - * - * @return true to use autoConnect feature, false (default) otherwise. - */ - protected boolean shouldAutoConnect() { - return false; - } - - @Override - public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) { - final int titleId = getLoggerProfileTitle(); - if (titleId > 0) { - logSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name); - // If nRF Logger is not installed we may want to use local logger - if (logSession == null && getLocalAuthorityLogger() != null) { - logSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name); - } - } - deviceName = name; - bleManager.setLogger(logSession); - bleManager.connect(device) - .useAutoConnect(shouldAutoConnect()) - .retry(3, 100) - .enqueue(); - } - - @Override - public void onDialogCanceled() { - // do nothing - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - runOnUiThread(() -> { - deviceNameView.setText(deviceName != null ? deviceName : getString(R.string.not_available)); - connectButton.setText(R.string.action_connecting); - }); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - deviceConnected = true; - runOnUiThread(() -> connectButton.setText(R.string.action_disconnect)); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - runOnUiThread(() -> connectButton.setText(R.string.action_disconnecting)); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - deviceConnected = false; - bleManager.close(); - runOnUiThread(() -> { - connectButton.setText(R.string.action_connect); - deviceNameView.setText(getDefaultDeviceName()); - }); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - deviceConnected = false; - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, boolean optionalServicesFound) { - // this may notify user or show some views - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - showToast(R.string.bonding); - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - showToast(R.string.bonded); - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - showToast(R.string.bonding_failed); - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode); - showToast(message + " (" + errorCode + ")"); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - showToast(R.string.not_supported); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message a message to be shown - */ - protected void showToast(final String message) { - runOnUiThread(() -> Toast.makeText(BleProfileExpandableListActivity.this, message, Toast.LENGTH_SHORT).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - runOnUiThread(() -> Toast.makeText(BleProfileExpandableListActivity.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns true if the device is connected. Services may not have been discovered yet. - */ - protected boolean isDeviceConnected() { - return deviceConnected; - } - - /** - * Returns the name of the device that the phone is currently connected to or was connected last time - */ - protected String getDeviceName() { - return deviceName; - } - - /** - * Initializes the Bluetooth Low Energy manager. A manager is used to communicate with profile's services. - * - * @return the manager that was created - */ - protected abstract LoggableBleManager initializeManager(); - - /** - * Restores the default UI before reconnecting - */ - protected abstract void setDefaultUI(); - - /** - * Returns the default device name resource id. The real device name is obtained when connecting to the device. This one is used when device has - * disconnected. - * - * @return the default device name resource id - */ - protected abstract int getDefaultDeviceName(); - - /** - * Returns the string resource id that will be shown in About box - * - * @return the about resource id - */ - protected abstract int getAboutTextId(); - - /** - * The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also: - * {@link #isChangingConfigurations()}. - * - * @return the required UUID or null - */ - protected abstract UUID getFilterUUID(); - - /** - * Shows the scanner fragment. - * - * @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their - * services - * @see #getFilterUUID() - */ - private void showDeviceScanningDialog(final UUID filter) { - runOnUiThread(() -> { - final ScannerFragment dialog = ScannerFragment.getInstance(filter); - dialog.show(getSupportFragmentManager(), "scan_fragment"); - }); - } - - /** - * Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT. - * - * @return the logger session or null - */ - protected ILogSession getLogSession() { - return logSession; - } - - private void ensureBLESupported() { - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { - Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show(); - finish(); - } - } - - protected boolean isBLEEnabled() { - final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); - final BluetoothAdapter adapter = bluetoothManager.getAdapter(); - return adapter != null && adapter.isEnabled(); - } - - protected void showBLEDialog() { - final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult(enableIntent, REQUEST_ENABLE_BT); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java deleted file mode 100644 index 29e7871d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileService.java +++ /dev/null @@ -1,609 +0,0 @@ -/* - * Copyright (c) 2015, 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.profile; - -import android.app.Service; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.Uri; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import android.util.Log; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.ble.BleManager; -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.ble.utils.ILogger; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.Logger; - -@SuppressWarnings("unused") -public abstract class BleProfileService extends Service implements BleManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "BleProfileService"; - - public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE"; - public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED"; - public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY"; - public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE"; - @Deprecated - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR"; - - /** - * The parameter passed when creating the service. Must contain the address of the sensor that we want to connect to - */ - public static final String EXTRA_DEVICE_ADDRESS = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_ADDRESS"; - /** - * The key for the device name that is returned in {@link #BROADCAST_CONNECTION_STATE} with state {@link #STATE_CONNECTED}. - */ - public static final String EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME"; - public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"; - public static final String EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI"; - public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE"; - public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE"; - public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY"; - public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY"; - @Deprecated - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE"; - public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE"; - - public static final int STATE_LINK_LOSS = -1; - public static final int STATE_DISCONNECTED = 0; - public static final int STATE_CONNECTED = 1; - public static final int STATE_CONNECTING = 2; - public static final int STATE_DISCONNECTING = 3; - - private LoggableBleManager bleManager; - private Handler handler; - - protected boolean bound; - private boolean activityIsChangingConfiguration; - private BluetoothDevice bluetoothDevice; - private String deviceName; - private ILogSession logSession; - - private final BroadcastReceiver bluetoothStateBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); - final ILogger logger = getBinder(); - - final String stateString = "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(state); - logger.log(Log.DEBUG, stateString); - - switch (state) { - case BluetoothAdapter.STATE_ON: - onBluetoothEnabled(); - break; - case BluetoothAdapter.STATE_TURNING_OFF: - case BluetoothAdapter.STATE_OFF: - onBluetoothDisabled(); - break; - } - } - - private String state2String(final int state) { - switch (state) { - case BluetoothAdapter.STATE_TURNING_ON: - return "TURNING ON"; - case BluetoothAdapter.STATE_ON: - return "ON"; - case BluetoothAdapter.STATE_TURNING_OFF: - return "TURNING OFF"; - case BluetoothAdapter.STATE_OFF: - return "OFF"; - default: - return "UNKNOWN (" + state + ")"; - } - } - }; - - public class LocalBinder extends Binder implements ILogger { - /** - * Disconnects from the sensor. - */ - public final void disconnect() { - final int state = bleManager.getConnectionState(); - if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) { - bleManager.close(); - onDeviceDisconnected(bluetoothDevice); - return; - } - - bleManager.disconnect().enqueue(); - } - - /** - * Sets whether the bound activity if changing configuration or not. - * If false, we will turn off battery level notifications in onUnbind(..) method below. - * - * @param changing true if the bound activity is finishing - */ - public void setActivityIsChangingConfiguration(final boolean changing) { - activityIsChangingConfiguration = changing; - } - - /** - * Returns the device address - * - * @return device address - */ - public String getDeviceAddress() { - return bluetoothDevice.getAddress(); - } - - /** - * Returns the device name - * - * @return the device name - */ - public String getDeviceName() { - return deviceName; - } - - /** - * Returns the Bluetooth device - * - * @return the Bluetooth device - */ - public BluetoothDevice getBluetoothDevice() { - return bluetoothDevice; - } - - /** - * Returns true if the device is connected to the sensor. - * - * @return true if device is connected to the sensor, false otherwise - */ - public boolean isConnected() { - return bleManager.isConnected(); - } - - - /** - * Returns the connection state of given device. - * - * @return the connection state, as in {@link BleManager#getConnectionState()}. - */ - public int getConnectionState() { - return bleManager.getConnectionState(); - } - - /** - * Returns the log session that can be used to append log entries. - * The log session is created when the service is being created. - * The method returns null if the nRF Logger app was not installed. - * - * @return the log session - */ - public ILogSession getLogSession() { - return logSession; - } - - @Override - public void log(final int level, @NonNull final String message) { - Logger.log(logSession, level, message); - } - - @Override - public void log(final int level, final @StringRes int messageRes, final Object... params) { - Logger.log(logSession, level, messageRes, params); - } - } - - /** - * Returns a handler that is created in onCreate(). - * The handler may be used to postpone execution of some operations or to run them in UI thread. - */ - protected Handler getHandler() { - return handler; - } - - /** - * Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity. - * - * @return the service binder - */ - protected LocalBinder getBinder() { - // default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation - return new LocalBinder(); - } - - @Override - public IBinder onBind(final Intent intent) { - bound = true; - return getBinder(); - } - - @Override - public final void onRebind(final Intent intent) { - bound = true; - - if (!activityIsChangingConfiguration) - onRebind(); - } - - /** - * Called when the activity has rebound to the service after being recreated. - * This method is not called when the activity was killed to be recreated when the phone orientation changed - * if prior to being killed called {@link BleProfileService.LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true. - */ - protected void onRebind() { - // empty default implementation - } - - @Override - public final boolean onUnbind(final Intent intent) { - bound = false; - - if (!activityIsChangingConfiguration) - onUnbind(); - - // We want the onRebind method be called if anything else binds to it again - return true; - } - - /** - * Called when the activity has unbound from the service before being finished. - * This method is not called when the activity is killed to be recreated when the phone orientation changed. - */ - protected void onUnbind() { - // empty default implementation - } - - @SuppressWarnings("unchecked") - @Override - public void onCreate() { - super.onCreate(); - - handler = new Handler(); - - // Initialize the manager - bleManager = initializeManager(); - bleManager.setGattCallbacks(this); - - // Register broadcast receivers - registerReceiver(bluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - - // Service has now been created - onServiceCreated(); - - // Call onBluetoothEnabled if Bluetooth enabled - final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (bluetoothAdapter.isEnabled()) { - onBluetoothEnabled(); - } - } - - /** - * Called when the service has been created, before the {@link #onBluetoothEnabled()} is called. - */ - protected void onServiceCreated() { - // empty default implementation - } - - /** - * Initializes the Ble Manager responsible for connecting to a single device. - * - * @return a new BleManager object - */ - @SuppressWarnings("rawtypes") - protected abstract LoggableBleManager initializeManager(); - - /** - * This method returns whether autoConnect option should be used. - * - * @return true to use autoConnect feature, false (default) otherwise. - */ - protected boolean shouldAutoConnect() { - return false; - } - - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - if (intent == null || !intent.hasExtra(EXTRA_DEVICE_ADDRESS)) - throw new UnsupportedOperationException("No device address at EXTRA_DEVICE_ADDRESS key"); - - final Uri logUri = intent.getParcelableExtra(EXTRA_LOG_URI); - logSession = Logger.openSession(getApplicationContext(), logUri); - deviceName = intent.getStringExtra(EXTRA_DEVICE_NAME); - - Logger.i(logSession, "Service started"); - - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - final String deviceAddress = intent.getStringExtra(EXTRA_DEVICE_ADDRESS); - bluetoothDevice = adapter.getRemoteDevice(deviceAddress); - - bleManager.setLogger(logSession); - onServiceStarted(); - bleManager.connect(bluetoothDevice) - .useAutoConnect(shouldAutoConnect()) - .retry(3, 100) - .enqueue(); - return START_REDELIVER_INTENT; - } - - /** - * Called when the service has been started. The device name and address are set. - * The BLE Manager will try to connect to the device after this method finishes. - */ - protected void onServiceStarted() { - // empty default implementation - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - // This method is called when user removed the app from Recents. - // 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(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - // Unregister broadcast receivers - unregisterReceiver(bluetoothStateBroadcastReceiver); - - // shutdown the manager - bleManager.close(); - Logger.i(logSession, "Service destroyed"); - bleManager = null; - bluetoothDevice = null; - deviceName = null; - logSession = null; - handler = null; - } - - /** - * Method called when Bluetooth Adapter has been disabled. - */ - protected void onBluetoothDisabled() { - // empty default implementation - } - - /** - * This method is called when Bluetooth Adapter has been enabled and - * after the service was created if Bluetooth Adapter was enabled at that moment. - * This method could initialize all Bluetooth related features, for example open the GATT server. - */ - protected void onBluetoothEnabled() { - // empty default implementation - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_DEVICE_NAME, deviceName); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - // Notify user about changing the state to DISCONNECTING - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * This method should return false if the service needs to do some asynchronous work after if has disconnected from the device. - * In that case the {@link #stopService()} method must be called when done. - * - * @return true (default) to automatically stop the service when device is disconnected. False otherwise. - */ - protected boolean stopWhenDisconnected() { - return true; - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - // Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above - - // Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be - // invoked ONLY when user requested disconnection (using Disconnect button). If the device - // disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead. - - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - if (stopWhenDisconnected()) - stopService(); - } - - protected void stopService() { - // user requested disconnection. We must stop the service - Logger.v(logSession, "Stopping service..."); - stopSelf(); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_DEVICE_READY); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - // no need for disconnecting, it will be disconnected by the manager automatically - } - - @Override - public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonded); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding_failed); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - final Intent broadcast = new Intent(BROADCAST_ERROR); - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice); - broadcast.putExtra(EXTRA_ERROR_MESSAGE, message); - broadcast.putExtra(EXTRA_ERROR_CODE, errorCode); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - handler.post(() -> Toast.makeText(BleProfileService.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message a message to be shown - */ - protected void showToast(final String message) { - handler.post(() -> Toast.makeText(BleProfileService.this, message, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns the log session that can be used to append log entries. The method returns null if the nRF Logger app was not installed. It is safe to use logger when - * {@link #onServiceStarted()} has been called. - * - * @return the log session - */ - protected ILogSession getLogSession() { - return logSession; - } - - /** - * Returns the device address - * - * @return device address - */ - protected String getDeviceAddress() { - return bluetoothDevice.getAddress(); - } - - /** - * Returns the Bluetooth device object - * - * @return bluetooth device - */ - protected BluetoothDevice getBluetoothDevice() { - return bluetoothDevice; - } - - /** - * Returns the device name - * - * @return the device name - */ - protected String getDeviceName() { - return deviceName; - } - - /** - * Returns true if the device is connected to the sensor. - * - * @return true if device is connected to the sensor, false otherwise - */ - protected boolean isConnected() { - return bleManager != null && bleManager.isConnected(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java deleted file mode 100644 index c0447a73..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/BleProfileServiceReadyActivity.java +++ /dev/null @@ -1,673 +0,0 @@ -/* - * Copyright (c) 2015, 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.profile; - -import android.app.Service; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import java.util.UUID; - -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.LocalLogSession; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.AppHelpFragment; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; - -/** - *

- * The {@link BleProfileServiceReadyActivity} activity is designed to be the base class for profile activities that uses services in order to connect to the - * device. When user press CONNECT button a service is created and the activity binds to it. The service tries to connect to the service and notifies the - * activity using Local Broadcasts ({@link LocalBroadcastManager}). See {@link BleProfileService} for messages. If the device is not in range it will listen for - * it and connect when it become visible. The service exists until user will press DISCONNECT button. - *

- *

- * When user closes the activity (f.e. by pressing Back button) while being connected, the Service remains working. It's still connected to the device or still - * listens for it. When entering back to the activity, activity will to bind to the service and refresh UI. - *

- */ -@SuppressWarnings("unused") -public abstract class BleProfileServiceReadyActivity extends AppCompatActivity implements - ScannerFragment.OnDeviceSelectedListener, BleManagerCallbacks { - private static final String TAG = "BleProfileServiceReadyActivity"; - - private static final String SIS_DEVICE_NAME = "device_name"; - private static final String SIS_DEVICE = "device"; - private static final String LOG_URI = "log_uri"; - protected static final int REQUEST_ENABLE_BT = 2; - - private E service; - - private TextView deviceNameView; - private Button connectButton; - - private ILogSession logSession; - private BluetoothDevice bluetoothDevice; - private String deviceName; - - private final BroadcastReceiver commonBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - // Check if the broadcast applies the connected device - if (!isBroadcastForThisDevice(intent)) - return; - - final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BleProfileService.EXTRA_DEVICE); - if (bluetoothDevice == null) - return; - - final String action = intent.getAction(); - switch (action) { - case BleProfileService.BROADCAST_CONNECTION_STATE: { - final int state = intent.getIntExtra(BleProfileService.EXTRA_CONNECTION_STATE, BleProfileService.STATE_DISCONNECTED); - - switch (state) { - case BleProfileService.STATE_CONNECTED: { - deviceName = intent.getStringExtra(BleProfileService.EXTRA_DEVICE_NAME); - onDeviceConnected(bluetoothDevice); - break; - } - case BleProfileService.STATE_DISCONNECTED: { - onDeviceDisconnected(bluetoothDevice); - deviceName = null; - break; - } - case BleProfileService.STATE_LINK_LOSS: { - onLinkLossOccurred(bluetoothDevice); - break; - } - case BleProfileService.STATE_CONNECTING: { - onDeviceConnecting(bluetoothDevice); - break; - } - case BleProfileService.STATE_DISCONNECTING: { - onDeviceDisconnecting(bluetoothDevice); - break; - } - default: - // there should be no other actions - break; - } - break; - } - case BleProfileService.BROADCAST_SERVICES_DISCOVERED: { - final boolean primaryService = intent.getBooleanExtra(BleProfileService.EXTRA_SERVICE_PRIMARY, false); - final boolean secondaryService = intent.getBooleanExtra(BleProfileService.EXTRA_SERVICE_SECONDARY, false); - - if (primaryService) { - onServicesDiscovered(bluetoothDevice, secondaryService); - } else { - onDeviceNotSupported(bluetoothDevice); - } - break; - } - case BleProfileService.BROADCAST_DEVICE_READY: { - onDeviceReady(bluetoothDevice); - break; - } - case BleProfileService.BROADCAST_BOND_STATE: { - final int state = intent.getIntExtra(BleProfileService.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - switch (state) { - case BluetoothDevice.BOND_BONDING: - onBondingRequired(bluetoothDevice); - break; - case BluetoothDevice.BOND_BONDED: - onBonded(bluetoothDevice); - break; - } - break; - } - case BleProfileService.BROADCAST_ERROR: { - final String message = intent.getStringExtra(BleProfileService.EXTRA_ERROR_MESSAGE); - final int errorCode = intent.getIntExtra(BleProfileService.EXTRA_ERROR_CODE, 0); - onError(bluetoothDevice, message, errorCode); - break; - } - } - } - }; - - private ServiceConnection serviceConnection = new ServiceConnection() { - @SuppressWarnings("unchecked") - @Override - public void onServiceConnected(final ComponentName name, final IBinder service) { - final E bleService = BleProfileServiceReadyActivity.this.service = (E) service; - bluetoothDevice = bleService.getBluetoothDevice(); - logSession = bleService.getLogSession(); - Logger.d(logSession, "Activity bound to the service"); - onServiceBound(bleService); - - // Update UI - deviceName = bleService.getDeviceName(); - deviceNameView.setText(deviceName); - connectButton.setText(R.string.action_disconnect); - - // And notify user if device is connected - if (bleService.isConnected()) { - onDeviceConnected(bluetoothDevice); - } else { - // If the device is not connected it means that either it is still connecting, - // or the link was lost and service is trying to connect to it (autoConnect=true). - onDeviceConnecting(bluetoothDevice); - } - } - - @Override - public void onServiceDisconnected(final ComponentName name) { - // Note: this method is called only when the service is killed by the system, - // not when it stops itself or is stopped by the activity. - // It will be called only when there is critically low memory, in practice never - // when the activity is in foreground. - Logger.d(logSession, "Activity disconnected from the service"); - deviceNameView.setText(getDefaultDeviceName()); - connectButton.setText(R.string.action_connect); - - service = null; - deviceName = null; - bluetoothDevice = null; - logSession = null; - onServiceUnbound(); - } - }; - - @Override - protected final void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - ensureBLESupported(); - if (!isBLEEnabled()) { - showBLEDialog(); - } - - // Restore the old log session - if (savedInstanceState != null) { - final Uri logUri = savedInstanceState.getParcelable(LOG_URI); - logSession = Logger.openSession(getApplicationContext(), logUri); - } - - // In onInitialize method a final class may register local broadcast receivers that will listen for events from the service - onInitialize(savedInstanceState); - // The onCreateView class should... create the view - onCreateView(savedInstanceState); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - - // Common nRF Toolbox view references are obtained here - setUpView(); - // View is ready to be used - onViewCreated(savedInstanceState); - - LocalBroadcastManager.getInstance(this).registerReceiver(commonBroadcastReceiver, makeIntentFilter()); - } - - @Override - protected void onStart() { - super.onStart(); - - /* - * If the service has not been started before, the following lines will not start it. - * However, if it's running, the Activity will bind to it and notified via serviceConnection. - */ - final Intent service = new Intent(this, getServiceClass()); - // We pass 0 as a flag so the service will not be created if not exists. - bindService(service, serviceConnection, 0); - - /* - * When user exited the UARTActivity while being connected, the log session is kept in - * the service. We may not get it before binding to it so in this case this event will - * not be logged (logSession is null until onServiceConnected(..) is called). - * It will, however, be logged after the orientation changes. - */ - } - - @Override - protected void onStop() { - super.onStop(); - - try { - // We don't want to perform some operations (e.g. disable Battery Level notifications) - // in the service if we are just rotating the screen. However, when the activity will - // disappear, we may want to disable some device features to reduce the battery - // consumption. - if (service != null) - service.setActivityIsChangingConfiguration(isChangingConfigurations()); - - unbindService(serviceConnection); - service = null; - - Logger.d(logSession, "Activity unbound from the service"); - onServiceUnbound(); - deviceName = null; - bluetoothDevice = null; - logSession = null; - } catch (final IllegalArgumentException e) { - // do nothing, we were not connected to the sensor - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - LocalBroadcastManager.getInstance(this).unregisterReceiver(commonBroadcastReceiver); - } - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(BleProfileService.BROADCAST_CONNECTION_STATE); - intentFilter.addAction(BleProfileService.BROADCAST_SERVICES_DISCOVERED); - intentFilter.addAction(BleProfileService.BROADCAST_DEVICE_READY); - intentFilter.addAction(BleProfileService.BROADCAST_BOND_STATE); - intentFilter.addAction(BleProfileService.BROADCAST_ERROR); - return intentFilter; - } - - /** - * Called when activity binds to the service. The parameter is the object returned in {@link Service#onBind(Intent)} method in your service. The method is - * called when device gets connected or is created while sensor was connected before. You may use the binder as a sensor interface. - */ - protected abstract void onServiceBound(E binder); - - /** - * Called when activity unbinds from the service. You may no longer use this binder because the sensor was disconnected. This method is also called when you - * leave the activity being connected to the sensor in the background. - */ - protected abstract void onServiceUnbound(); - - /** - * Returns the service class for sensor communication. The service class must derive from {@link BleProfileService} in order to operate with this class. - * - * @return the service class - */ - protected abstract Class getServiceClass(); - - /** - * Returns the service interface that may be used to communicate with the sensor. This will return null if the device is disconnected from the - * sensor. - * - * @return the service binder or null - */ - protected E getService() { - return service; - } - - /** - * You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created. - */ - protected void onInitialize(final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}. - * Use to obtain references to views. Connect/Disconnect button, the device name view are manager automatically. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - protected abstract void onCreateView(final Bundle savedInstanceState); - - /** - * Called after the view has been created. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - protected void onViewCreated(final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called after the view and the toolbar has been created. - */ - protected final void setUpView() { - // set GUI - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - connectButton = findViewById(R.id.action_connect); - deviceNameView = findViewById(R.id.device_name); - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString(SIS_DEVICE_NAME, deviceName); - outState.putParcelable(SIS_DEVICE, bluetoothDevice); - if (logSession != null) - outState.putParcelable(LOG_URI, logSession.getSessionUri()); - } - - @Override - protected void onRestoreInstanceState(final @NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - deviceName = savedInstanceState.getString(SIS_DEVICE_NAME); - bluetoothDevice = savedInstanceState.getParcelable(SIS_DEVICE); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.help, menu); - return true; - } - - /** - * Use this method to handle menu actions other than home and about. - * - * @param itemId the menu item id - * @return true if action has been handled - */ - protected boolean onOptionsItemSelected(final int itemId) { - // Overwrite when using menu other than R.menu.help - return false; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - final int id = item.getItemId(); - switch (id) { - case android.R.id.home: - onBackPressed(); - break; - case R.id.action_about: - final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId()); - fragment.show(getSupportFragmentManager(), "help_fragment"); - break; - default: - return onOptionsItemSelected(id); - } - return true; - } - - /** - * Called when user press CONNECT or DISCONNECT button. See layout files -> onClick attribute. - */ - public void onConnectClicked(final View view) { - if (isBLEEnabled()) { - if (service == null) { - setDefaultUI(); - showDeviceScanningDialog(getFilterUUID()); - } else { - service.disconnect(); - } - } else { - showBLEDialog(); - } - } - - /** - * Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used. - * - * @return the title resource id - */ - protected int getLoggerProfileTitle() { - return 0; - } - - /** - * This method may return the local log content provider authority if local log sessions are supported. - * - * @return local log session content provider URI - */ - protected Uri getLocalAuthorityLogger() { - return null; - } - - @Override - public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) { - final int titleId = getLoggerProfileTitle(); - if (titleId > 0) { - logSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name); - // If nRF Logger is not installed we may want to use local logger - if (logSession == null && getLocalAuthorityLogger() != null) { - logSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name); - } - } - bluetoothDevice = device; - deviceName = name; - - // The device may not be in the range but the service will try to connect to it if it reach it - Logger.d(logSession, "Creating service..."); - final Intent service = new Intent(this, getServiceClass()); - service.putExtra(BleProfileService.EXTRA_DEVICE_ADDRESS, device.getAddress()); - service.putExtra(BleProfileService.EXTRA_DEVICE_NAME, name); - if (logSession != null) - service.putExtra(BleProfileService.EXTRA_LOG_URI, logSession.getSessionUri()); - startService(service); - Logger.d(logSession, "Binding to the service..."); - bindService(service, serviceConnection, 0); - } - - @Override - public void onDialogCanceled() { - // do nothing - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - deviceNameView.setText(deviceName != null ? deviceName : getString(R.string.not_available)); - connectButton.setText(R.string.action_connecting); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - deviceNameView.setText(deviceName); - connectButton.setText(R.string.action_disconnect); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - connectButton.setText(R.string.action_disconnecting); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - connectButton.setText(R.string.action_connect); - deviceNameView.setText(getDefaultDeviceName()); - - try { - Logger.d(logSession, "Unbinding from the service..."); - unbindService(serviceConnection); - service = null; - - Logger.d(logSession, "Activity unbound from the service"); - onServiceUnbound(); - deviceName = null; - bluetoothDevice = null; - logSession = null; - } catch (final IllegalArgumentException e) { - // do nothing. This should never happen but does... - } - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // empty default implementation - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode); - showToast(message + " (" + errorCode + ")"); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - showToast(R.string.not_supported); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message a message to be shown - */ - protected void showToast(final String message) { - runOnUiThread(() -> Toast.makeText(BleProfileServiceReadyActivity.this, message, Toast.LENGTH_LONG).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - runOnUiThread(() -> Toast.makeText(BleProfileServiceReadyActivity.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns true if the device is connected. Services may not have been discovered yet. - */ - protected boolean isDeviceConnected() { - return service != null && service.isConnected(); - } - - /** - * Returns the name of the device that the phone is currently connected to or was connected last time - */ - protected String getDeviceName() { - return deviceName; - } - - /** - * Restores the default UI before reconnecting - */ - protected abstract void setDefaultUI(); - - /** - * Returns the default device name resource id. The real device name is obtained when connecting to the device. This one is used when device has - * disconnected. - * - * @return the default device name resource id - */ - protected abstract int getDefaultDeviceName(); - - /** - * Returns the string resource id that will be shown in About box - * - * @return the about resource id - */ - protected abstract int getAboutTextId(); - - /** - * The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also: - * {@link #isChangingConfigurations()}. - * - * @return the required UUID or null - */ - protected abstract UUID getFilterUUID(); - - /** - * Checks the {@link BleProfileService#EXTRA_DEVICE} in the given intent and compares it with the connected BluetoothDevice object. - * @param intent intent received via a broadcast from the service - * @return true if the data in the intent apply to the connected device, false otherwise - */ - protected boolean isBroadcastForThisDevice(final Intent intent) { - final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BleProfileService.EXTRA_DEVICE); - return bluetoothDevice != null && bluetoothDevice.equals(bluetoothDevice); - } - - /** - * Shows the scanner fragment. - * - * @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their - * services - * @see #getFilterUUID() - */ - private void showDeviceScanningDialog(final UUID filter) { - final ScannerFragment dialog = ScannerFragment.getInstance(filter); - dialog.show(getSupportFragmentManager(), "scan_fragment"); - } - - /** - * Returns the log session. Log session is created when the device was selected using the {@link ScannerFragment} and released when user press DISCONNECT. - * - * @return the logger session or null - */ - protected ILogSession getLogSession() { - return logSession; - } - - private void ensureBLESupported() { - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { - Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show(); - finish(); - } - } - - protected boolean isBLEEnabled() { - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - return adapter != null && adapter.isEnabled(); - } - - protected void showBLEDialog() { - final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult(enableIntent, REQUEST_ENABLE_BT); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/LoggableBleManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/LoggableBleManager.java deleted file mode 100644 index 2cd01d90..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/LoggableBleManager.java +++ /dev/null @@ -1,49 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.profile; - -import android.content.Context; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.ble.LegacyBleManager; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.log.Logger; - -/** - * The manager that logs to nRF Logger. If nRF Logger is not installed, logs are ignored. - * - * @param the callbacks class. - */ -public abstract class LoggableBleManager extends LegacyBleManager { - private ILogSession logSession; - - /** - * The manager constructor. - *

- * After constructing the manager, the callbacks object must be set with - * {@link #setGattCallbacks(BleManagerCallbacks)}. - * - * @param context the context. - */ - public LoggableBleManager(@NonNull final Context context) { - super(context); - } - - /** - * Sets the log session to log into. - * - * @param session nRF Logger log session to log inti, or null, if nRF Logger is not installed. - */ - public void setLogger(@Nullable final ILogSession session) { - logSession = session; - } - - @Override - public void log(final int priority, @NonNull final String message) { - Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message); - Log.println(priority, "BleManager", message); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileService.java deleted file mode 100644 index a4f4a602..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileService.java +++ /dev/null @@ -1,636 +0,0 @@ -/* - * Copyright (c) 2015, 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.profile.multiconnect; - -import android.app.Service; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.util.Log; -import android.widget.Toast; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import no.nordicsemi.android.ble.BleManager; -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.ble.utils.ILogger; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -public abstract class BleMulticonnectProfileService extends Service implements BleManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "BleMultiProfileService"; - - public static final String BROADCAST_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE"; - public static final String BROADCAST_SERVICES_DISCOVERED = "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED"; - public static final String BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY"; - public static final String BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE"; - @Deprecated - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR"; - - public static final String EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"; - public static final String EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE"; - public static final String EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE"; - public static final String EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY"; - public static final String EXTRA_SERVICE_SECONDARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY"; - @Deprecated - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - public static final String EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE"; - public static final String EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE"; - - public static final int STATE_LINK_LOSS = -1; - public static final int STATE_DISCONNECTED = 0; - public static final int STATE_CONNECTED = 1; - public static final int STATE_CONNECTING = 2; - public static final int STATE_DISCONNECTING = 3; - - private HashMap> bleManagers; - private List managedDevices; - private Handler handler; - - protected boolean bound; - private boolean activityIsChangingConfiguration; - - private final BroadcastReceiver bluetoothStateBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF); - final int previousState = intent.getIntExtra(BluetoothAdapter.EXTRA_PREVIOUS_STATE, BluetoothAdapter.STATE_OFF); - - switch (state) { - case BluetoothAdapter.STATE_ON: - // On older phones (tested on Nexus 4 with Android 5.0.1) the Bluetooth requires some time - // after it has been enabled before some operations can start. Starting the GATT server here - // without a delay is very likely to cause a DeadObjectException from BluetoothManager#openGattServer(...). - handler.postDelayed(() -> onBluetoothEnabled(), 600); - break; - case BluetoothAdapter.STATE_TURNING_OFF: - case BluetoothAdapter.STATE_OFF: - if (previousState != BluetoothAdapter.STATE_TURNING_OFF && previousState != BluetoothAdapter.STATE_OFF) - onBluetoothDisabled(); - break; - } - } - }; - - public class LocalBinder extends Binder implements ILogger, IDeviceLogger { - /** - * Returns an unmodifiable list of devices managed by the service. - * The returned devices do not need to be connected at tha moment. Each of them was however created - * using {@link #connect(BluetoothDevice)} method so they might have been connected before and disconnected. - * @return unmodifiable list of devices managed by the service - */ - public final List getManagedDevices() { - return Collections.unmodifiableList(managedDevices); - } - - /** - * Connects to the given device. If the device is already connected this method does nothing. - * @param device target Bluetooth device - */ - public void connect(final BluetoothDevice device) { - connect(device, null); - } - - /** - * Adds the given device to managed and stars connecting to it. If the device is already connected this method does nothing. - * @param device target Bluetooth device - * @param session log session that has to be used by the device - */ - @SuppressWarnings("unchecked") - public void connect(final BluetoothDevice device, final ILogSession session) { - // If a device is in managed devices it means that it's already connected, or was connected - // using autoConnect and the link was lost but Android is already trying to connect to it. - if (managedDevices.contains(device)) - return; - managedDevices.add(device); - - LoggableBleManager manager = bleManagers.get(device); - if (manager == null) { - bleManagers.put(device, manager = initializeManager()); - manager.setGattCallbacks(BleMulticonnectProfileService.this); - } - manager.setLogger(session); - manager.connect(device) - .retry(2, 100) - .useAutoConnect(shouldAutoConnect()) - .timeout(10000) - .fail((d, status) -> { - managedDevices.remove(device); - bleManagers.remove(device); - }) - .enqueue(); - } - - /** - * Disconnects the given device and removes the associated BleManager object. - * If the list of BleManagers is empty while the last activity unbinds from the service, - * the service will stop itself. - * @param device target device to disconnect and forget - */ - public void disconnect(final BluetoothDevice device) { - final BleManager manager = bleManagers.get(device); - if (manager != null && manager.isConnected()) { - manager.disconnect().enqueue(); - } - managedDevices.remove(device); - } - - /** - * Returns true if the device is connected to the sensor. - * @param device the target device - * @return true if device is connected to the sensor, false otherwise - */ - public final boolean isConnected(final BluetoothDevice device) { - final BleManager manager = bleManagers.get(device); - return manager != null && manager.isConnected(); - } - - /** - * Returns true if the device has finished initializing. - * @param device the target device - * @return true if device is connected to the sensor and has finished - * initializing. False otherwise. - */ - public final boolean isReady(final BluetoothDevice device) { - final BleManager manager = bleManagers.get(device); - return manager != null && manager.isReady(); - } - - /** - * Returns the connection state of given device. - * @param device the target device - * @return the connection state, as in {@link BleManager#getConnectionState()}. - */ - @SuppressWarnings("unused") - public final int getConnectionState(final BluetoothDevice device) { - final BleManager manager = bleManagers.get(device); - return manager != null ? manager.getConnectionState() : BluetoothGatt.STATE_DISCONNECTED; - } - - /** - * Returns the last received battery level value. - * @param device the device of which battery level should be returned - * @return battery value or -1 if no value was received or Battery Level characteristic was not found - * @deprecated Keep battery value in your manager instead. - */ - @SuppressWarnings("deprecation") - @Deprecated - public int getBatteryValue(final BluetoothDevice device) { - final BleManager manager = bleManagers.get(device); - if (manager != null) - return manager.getBatteryValue(); - return 0; - } - - /** - * Sets whether the bound activity if changing configuration or not. - * If false, we will turn off battery level notifications in onUnbind(..) method below. - * @param changing true if the bound activity is finishing - */ - @SuppressWarnings("WeakerAccess") - public final void setActivityIsChangingConfiguration(final boolean changing) { - activityIsChangingConfiguration = changing; - } - - @Override - public void log(@NonNull final BluetoothDevice device, final int level, final String message) { - final BleManager manager = bleManagers.get(device); - if (manager != null) - manager.log(level, message); - } - - @Override - public void log(@NonNull final BluetoothDevice device, final int level, @StringRes final int messageRes, final Object... params) { - final BleManager manager = bleManagers.get(device); - if (manager != null) - manager.log(level, messageRes, params); - } - - @Override - public void log(final int level, @NonNull final String message) { - for (final BleManager manager : bleManagers.values()) - manager.log(level, message); - } - - @Override - public void log(final int level, @StringRes final int messageRes, final Object... params) { - for (final BleManager manager : bleManagers.values()) - manager.log(level, messageRes, params); - } - } - - /** - * Returns a handler that is created in onCreate(). - * The handler may be used to postpone execution of some operations or to run them in UI thread. - */ - protected Handler getHandler() { - return handler; - } - - /** - * This method returns whether autoConnect option should be used. - * - * @return true to use autoConnect feature, false (default) otherwise. - */ - protected boolean shouldAutoConnect() { - return false; - } - - /** - * Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity. - * - * @return the service binder - */ - protected LocalBinder getBinder() { - // default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation - return new LocalBinder(); - } - - @Override - public IBinder onBind(final Intent intent) { - bound = true; - return getBinder(); - } - - @Override - public final void onRebind(final Intent intent) { - bound = true; - - if (!activityIsChangingConfiguration) { - onRebind(); - } - } - - /** - * Called when the activity has rebound to the service after being recreated. - * This method is not called when the activity was killed to be recreated when the phone orientation changed - * if prior to being killed called {@link LocalBinder#setActivityIsChangingConfiguration(boolean)} with parameter true. - */ - protected void onRebind() { - // empty default implementation - } - - @Override - public final boolean onUnbind(final Intent intent) { - bound = false; - - if (!activityIsChangingConfiguration) { - if (!managedDevices.isEmpty()) { - onUnbind(); - } else { - // The last activity has disconnected from the service and there are no devices to manage. The service may be stopped. - stopSelf(); - } - } - - // We want the onRebind method be called if anything else binds to it again - return true; - } - - /** - * Called when the activity has unbound from the service before being finished. - * This method is not called when the activity is killed to be recreated when the phone orientation changed. - */ - protected void onUnbind() { - // empty default implementation - } - - @Override - public void onCreate() { - super.onCreate(); - - handler = new Handler(); - - // Initialize the map of BLE managers - bleManagers = new HashMap<>(); - managedDevices = new ArrayList<>(); - - // Register broadcast receivers - registerReceiver(bluetoothStateBroadcastReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - - // Service has now been created - onServiceCreated(); - - // Call onBluetoothEnabled if Bluetooth enabled - final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - if (bluetoothAdapter.isEnabled()) { - onBluetoothEnabled(); - } - } - - /** - * Called when the service has been created, before the {@link #onBluetoothEnabled()} is called. - */ - protected void onServiceCreated() { - // empty default implementation - } - - /** - * Initializes the Ble Manager responsible for connecting to a single device. - * @return a new BleManager object - */ - @SuppressWarnings("rawtypes") - protected abstract LoggableBleManager initializeManager(); - - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - onServiceStarted(); - // The service does not save addresses of managed devices. - // A bound activity will be required to add connections again. - return START_NOT_STICKY; - } - - /** - * Called when the service has been started. - */ - protected void onServiceStarted() { - // empty default implementation - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - // This method is called when user removed the app from Recents. - // 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(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - onServiceStopped(); - handler = null; - } - - /** - * Called when the service has been stopped. - */ - protected void onServiceStopped() { - // Unregister broadcast receivers - unregisterReceiver(bluetoothStateBroadcastReceiver); - - // The managers map may not be empty if the service was killed by the system - for (final BleManager manager : bleManagers.values()) { - // Service is being destroyed, no need to disconnect manually. - manager.close(); - manager.log(Log.INFO, "Service destroyed"); - } - bleManagers.clear(); - managedDevices.clear(); - bleManagers = null; - managedDevices = null; - } - - /** - * Method called when Bluetooth Adapter has been disabled. - */ - protected void onBluetoothDisabled() { - // do nothing, BleManagers have their own Bluetooth State broadcast received and will close themselves - } - - /** - * This method is called when Bluetooth Adapter has been enabled. It is also called - * after the service was created if Bluetooth Adapter was enabled at that moment. - * This method could initialize all Bluetooth related features, for example open the GATT server. - * Make sure you call super.onBluetoothEnabled() at this methods reconnects to - * devices that were connected before the Bluetooth was turned off. - */ - protected void onBluetoothEnabled() { - for (final BluetoothDevice device : managedDevices) { - final BleManager manager = bleManagers.get(device); - if (manager != null && !manager.isConnected()) - manager.connect(device).enqueue(); - } - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING); - LocalBroadcastManager.getInstance(BleMulticonnectProfileService.this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - // Note: if BleManager#shouldAutoConnect() for this device returned true, this callback will be - // invoked ONLY when user requested disconnection (using Disconnect button). If the device - // disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead. - - // We no longer want to keep the device in the service - managedDevices.remove(device); - // The BleManager is not removed from the HashMap in order to keep the device's log session. - // bleManagers.remove(device); - - // Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - // When user disconnected the last device while the activity was not bound the service can be stopped - if (!bound && managedDevices.isEmpty()) { - stopSelf(); - } - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_CONNECTION_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - final Intent broadcast = new Intent(BROADCAST_DEVICE_READY); - broadcast.putExtra(EXTRA_DEVICE, device); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - // We don't like this device, remove it from both collections - managedDevices.remove(device); - bleManagers.remove(device); - - final Intent broadcast = new Intent(BROADCAST_SERVICES_DISCOVERED); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false); - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - // no need for disconnecting, it will be disconnected by the manager automatically - } - - @SuppressWarnings("deprecation") - @Override - public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonded); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - showToast(no.nordicsemi.android.nrftoolbox.common.R.string.bonding_failed); - - final Intent broadcast = new Intent(BROADCAST_BOND_STATE); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - final Intent broadcast = new Intent(BROADCAST_ERROR); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_ERROR_MESSAGE, message); - broadcast.putExtra(EXTRA_ERROR_CODE, errorCode); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId - * an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - handler.post(() -> Toast.makeText(BleMulticonnectProfileService.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message - * a message to be shown - */ - protected void showToast(final String message) { - handler.post(() -> Toast.makeText(BleMulticonnectProfileService.this, message, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns the {@link BleManager} object associated with given device, or null if such has not been created. - * To create a BleManager call the {@link LocalBinder#connect(BluetoothDevice)} method must be called. - * @param device the target device - * @return the BleManager or null - */ - protected BleManager getBleManager(final BluetoothDevice device) { - return bleManagers.get(device); - } - - /** - * Returns unmodifiable list of all managed devices. They don't have to be connected at the moment. - * @return list of managed devices - */ - protected List getManagedDevices() { - return Collections.unmodifiableList(managedDevices); - } - - /** - * Returns a list of those managed devices that are connected at the moment. - * @return list of connected devices - */ - protected List getConnectedDevices() { - final List list = new ArrayList<>(); - for (BluetoothDevice device : managedDevices) { - final BleManager manager = bleManagers.get(device); - if (manager != null && manager.isConnected()) - list.add(device); - } - return Collections.unmodifiableList(list); - } - - /** - * Returns true if the device is connected to the sensor. - * @param device the target device - * @return true if device is connected to the sensor, false otherwise - */ - protected boolean isConnected(final BluetoothDevice device) { - final BleManager manager = bleManagers.get(device); - return manager != null && manager.isConnected(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileServiceReadyActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileServiceReadyActivity.java deleted file mode 100644 index 1c3db523..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/BleMulticonnectProfileServiceReadyActivity.java +++ /dev/null @@ -1,557 +0,0 @@ -/* - * Copyright (c) 2015, 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.profile.multiconnect; - -import android.app.Service; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.IBinder; -import android.util.Log; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.Toast; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.ble.BleManagerCallbacks; -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.LocalLogSession; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.AppHelpFragment; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.scanner.ScannerFragment; -import no.nordicsemi.android.nrftoolbox.utility.DebugLogger; - -/** - *

- * The {@link BleMulticonnectProfileServiceReadyActivity} activity is designed to be the base class for profile activities that uses services in order to connect - * more than one device at the same time. A service extending {@link BleMulticonnectProfileService} is created when the activity is created, and the activity binds to it. - * The service returns a binder that may be used to connect, disconnect or manage devices, and notifies the - * activity using Local Broadcasts ({@link LocalBroadcastManager}). See {@link BleMulticonnectProfileService} for messages. If the device is not in range it will listen for - * it and connect when it become visible. The service exists until all managed devices have been disconnected and unmanaged and the last activity unbinds from it. - *

- *

- * When user closes the activity (e.g. by pressing Back button) while being connected, the Service remains working. It's remains connected to the devices or still - * listens for updates from them. When entering back to the activity, activity will to bind to the service and refresh UI. - *

- */ -@SuppressWarnings("unused") -public abstract class BleMulticonnectProfileServiceReadyActivity extends AppCompatActivity implements - ScannerFragment.OnDeviceSelectedListener, BleManagerCallbacks { - private static final String TAG = "BleMulticonnectProfileServiceReadyActivity"; - - protected static final int REQUEST_ENABLE_BT = 2; - - private E service; - private List managedDevices; - - private final BroadcastReceiver commonBroadcastReceiver = new BroadcastReceiver() { - @SuppressWarnings("deprecation") - @Override - public void onReceive(final Context context, final Intent intent) { - final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BleMulticonnectProfileService.EXTRA_DEVICE); - if (bluetoothDevice == null) - return; - - final String action = intent.getAction(); - switch (action) { - case BleMulticonnectProfileService.BROADCAST_CONNECTION_STATE: { - final int state = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_CONNECTION_STATE, BleMulticonnectProfileService.STATE_DISCONNECTED); - - switch (state) { - case BleMulticonnectProfileService.STATE_CONNECTED: { - onDeviceConnected(bluetoothDevice); - break; - } - case BleMulticonnectProfileService.STATE_DISCONNECTED: { - onDeviceDisconnected(bluetoothDevice); - break; - } - case BleMulticonnectProfileService.STATE_LINK_LOSS: { - onLinkLossOccurred(bluetoothDevice); - break; - } - case BleMulticonnectProfileService.STATE_CONNECTING: { - onDeviceConnecting(bluetoothDevice); - break; - } - case BleMulticonnectProfileService.STATE_DISCONNECTING: { - onDeviceDisconnecting(bluetoothDevice); - break; - } - default: - // there should be no other actions - break; - } - break; - } - case BleMulticonnectProfileService.BROADCAST_SERVICES_DISCOVERED: { - final boolean primaryService = intent.getBooleanExtra(BleMulticonnectProfileService.EXTRA_SERVICE_PRIMARY, false); - final boolean secondaryService = intent.getBooleanExtra(BleMulticonnectProfileService.EXTRA_SERVICE_SECONDARY, false); - - if (primaryService) { - onServicesDiscovered(bluetoothDevice, secondaryService); - } else { - onDeviceNotSupported(bluetoothDevice); - } - break; - } - case BleMulticonnectProfileService.BROADCAST_DEVICE_READY: { - onDeviceReady(bluetoothDevice); - break; - } - case BleMulticonnectProfileService.BROADCAST_BOND_STATE: { - final int state = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); - switch (state) { - case BluetoothDevice.BOND_BONDING: - onBondingRequired(bluetoothDevice); - break; - case BluetoothDevice.BOND_BONDED: - onBonded(bluetoothDevice); - break; - } - break; - } - case BleMulticonnectProfileService.BROADCAST_BATTERY_LEVEL: { - final int value = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_BATTERY_LEVEL, -1); - if (value > 0) - onBatteryValueReceived(bluetoothDevice, value); - break; - } - case BleMulticonnectProfileService.BROADCAST_ERROR: { - final String message = intent.getStringExtra(BleMulticonnectProfileService.EXTRA_ERROR_MESSAGE); - final int errorCode = intent.getIntExtra(BleMulticonnectProfileService.EXTRA_ERROR_CODE, 0); - onError(bluetoothDevice, message, errorCode); - break; - } - } - } - }; - - private ServiceConnection serviceConnection = new ServiceConnection() { - @SuppressWarnings("unchecked") - @Override - public void onServiceConnected(final ComponentName name, final IBinder service) { - final E bleService = BleMulticonnectProfileServiceReadyActivity.this.service = (E) service; - bleService.log(Log.DEBUG, "Activity bound to the service"); - managedDevices.addAll(bleService.getManagedDevices()); - onServiceBound(bleService); - - // and notify user if device is connected and ready - for (final BluetoothDevice device : managedDevices) { - if (bleService.isConnected(device)) - onDeviceConnected(device); - if (bleService.isReady(device)) - onDeviceReady(device); - } - } - - @Override - public void onServiceDisconnected(final ComponentName name) { - service = null; - onServiceUnbound(); - } - }; - - @Override - protected final void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - managedDevices = new ArrayList<>(); - - ensureBLESupported(); - if (!isBLEEnabled()) { - showBLEDialog(); - } - - // In onInitialize method a final class may register local broadcast receivers that will listen for events from the service - onInitialize(savedInstanceState); - // The onCreateView class should... create the view - onCreateView(savedInstanceState); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - - // Common nRF Toolbox view references are obtained here - setUpView(); - // View is ready to be used - onViewCreated(savedInstanceState); - - LocalBroadcastManager.getInstance(this).registerReceiver(commonBroadcastReceiver, makeIntentFilter()); - } - - @Override - protected void onStart() { - super.onStart(); - - /* - * In comparison to BleProfileServiceReadyActivity this activity always starts the service when started. - * Connecting to a device is done by calling service.connect(BluetoothDevice) method, not startService(...) like there. - * The service will stop itself when all devices it manages were disconnected and unmanaged and the last activity unbinds from it. - */ - final Intent service = new Intent(this, getServiceClass()); - startService(service); - bindService(service, serviceConnection, 0); - } - - @Override - protected void onStop() { - super.onStop(); - - if (service != null) { - // We don't want to perform some operations (e.g. disable Battery Level notifications) in the service if we are just rotating the screen. - // However, when the activity will disappear, we may want to disable some device features to reduce the battery consumption. - service.setActivityIsChangingConfiguration(isChangingConfigurations()); - // Log it here as there is no callback when the service gets unbound - // and the service will not be available later (the activity doesn't keep log sessions) - service.log(Log.DEBUG, "Activity unbound from the service"); - } - - unbindService(serviceConnection); - service = null; - - onServiceUnbound(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - - LocalBroadcastManager.getInstance(this).unregisterReceiver(commonBroadcastReceiver); - } - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_CONNECTION_STATE); - intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_SERVICES_DISCOVERED); - intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_DEVICE_READY); - intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_BOND_STATE); - intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_BATTERY_LEVEL); - intentFilter.addAction(BleMulticonnectProfileService.BROADCAST_ERROR); - return intentFilter; - } - - /** - * Called when activity binds to the service. The parameter is the object returned in {@link Service#onBind(Intent)} method in your service. - * It is safe to obtain managed devices now. - */ - protected abstract void onServiceBound(E binder); - - /** - * Called when activity unbinds from the service. You may no longer use this binder methods. - */ - protected abstract void onServiceUnbound(); - - /** - * Returns the service class for sensor communication. The service class must derive from {@link BleMulticonnectProfileService} in order to operate with this class. - * - * @return the service class - */ - protected abstract Class getServiceClass(); - - /** - * Returns the service interface that may be used to communicate with the sensor. This will return null if the device is disconnected from the - * sensor. - * - * @return the service binder or null - */ - protected E getService() { - return service; - } - - /** - * You may do some initialization here. This method is called from {@link #onCreate(Bundle)} before the view was created. - */ - protected void onInitialize(final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called from {@link #onCreate(Bundle)}. This method should build the activity UI, i.e. using {@link #setContentView(int)}. - * Use to obtain references to views. Connect/Disconnect button and the device name view are manager automatically. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - protected abstract void onCreateView(final Bundle savedInstanceState); - - /** - * Called after the view has been created. - * - * @param savedInstanceState contains the data it most recently supplied in {@link #onSaveInstanceState(Bundle)}. - * Note: Otherwise it is null. - */ - protected void onViewCreated(@SuppressWarnings("unused") final Bundle savedInstanceState) { - // empty default implementation - } - - /** - * Called after the view and the toolbar has been created. - */ - protected final void setUpView() { - // set GUI - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.help, menu); - return true; - } - - /** - * Use this method to handle menu actions other than home and about. - * - * @param itemId the menu item id - * @return true if action has been handled - */ - protected boolean onOptionsItemSelected(@SuppressWarnings("unused") final int itemId) { - // Overwrite when using menu other than R.menu.help - return false; - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - final int id = item.getItemId(); - switch (id) { - case android.R.id.home: - onBackPressed(); - break; - case R.id.action_about: - final AppHelpFragment fragment = AppHelpFragment.getInstance(getAboutTextId()); - fragment.show(getSupportFragmentManager(), "help_fragment"); - break; - default: - return onOptionsItemSelected(id); - } - return true; - } - - /** - * Called when user press ADD DEVICE button. See layout files -> onClick attribute. - */ - public void onAddDeviceClicked(final View view) { - if (isBLEEnabled()) { - showDeviceScanningDialog(getFilterUUID()); - } else { - showBLEDialog(); - } - } - - /** - * Returns the title resource id that will be used to create logger session. If 0 is returned (default) logger will not be used. - * - * @return the title resource id - */ - protected int getLoggerProfileTitle() { - return 0; - } - - /** - * This method may return the local log content provider authority if local log sessions are supported. - * - * @return local log session content provider URI - */ - protected Uri getLocalAuthorityLogger() { - return null; - } - - @Override - public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) { - final int titleId = getLoggerProfileTitle(); - ILogSession logSession = null; - if (titleId > 0) { - logSession = Logger.newSession(getApplicationContext(), getString(titleId), device.getAddress(), name); - // If nRF Logger is not installed we may want to use local logger - if (logSession == null && getLocalAuthorityLogger() != null) { - logSession = LocalLogSession.newSession(getApplicationContext(), getLocalAuthorityLogger(), device.getAddress(), name); - } - } - - service.connect(device, logSession); - } - - @Override - public void onDialogCanceled() { - // do nothing - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // empty default implementation - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBondingRequired(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBonded(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onBondingFailed(@NonNull final BluetoothDevice device) { - // empty default implementation - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - showToast(R.string.not_supported); - } - - @SuppressWarnings("deprecation") - @Override - public final boolean shouldEnableBatteryLevelNotifications(@NonNull final BluetoothDevice device) { - // This method will never be called. - // Please see BleMulticonnectProfileService#shouldEnableBatteryLevelNotifications(BluetoothDevice) instead. - throw new UnsupportedOperationException("This method should not be called"); - } - - @SuppressWarnings("deprecation") - @Override - public void onBatteryValueReceived(@NonNull final BluetoothDevice device, final int value) { - // empty default implementation - } - - @Override - public void onError(@NonNull final BluetoothDevice device, @NonNull final String message, final int errorCode) { - DebugLogger.e(TAG, "Error occurred: " + message + ", error code: " + errorCode); - showToast(message + " (" + errorCode + ")"); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param message a message to be shown - */ - protected void showToast(final String message) { - runOnUiThread(() -> Toast.makeText(BleMulticonnectProfileServiceReadyActivity.this, message, Toast.LENGTH_LONG).show()); - } - - /** - * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread - * - * @param messageResId an resource id of the message to be shown - */ - protected void showToast(final int messageResId) { - runOnUiThread(() -> Toast.makeText(BleMulticonnectProfileServiceReadyActivity.this, messageResId, Toast.LENGTH_SHORT).show()); - } - - /** - * Returns the string resource id that will be shown in About box - * - * @return the about resource id - */ - protected abstract int getAboutTextId(); - - /** - * The UUID filter is used to filter out available devices that does not have such UUID in their advertisement packet. See also: - * {@link #isChangingConfigurations()}. - * - * @return the required UUID or null - */ - protected abstract UUID getFilterUUID(); - - /** - * Returns unmodifiable list of managed devices. Managed device is a device the was selected on ScannerFragment until it's removed from the managed list. - * It does not have to be connected at that moment. - * @return unmodifiable list of managed devices - */ - protected List getManagedDevices() { - return Collections.unmodifiableList(managedDevices); - } - - /** - * Returns true if the device is connected. Services may not have been discovered yet. - * @param device the device to check if it's connected - */ - protected boolean isDeviceConnected(final BluetoothDevice device) { - return service != null && service.isConnected(device); - } - - /** - * Shows the scanner fragment. - * - * @param filter the UUID filter used to filter out available devices. The fragment will always show all bonded devices as there is no information about their - * services - * @see #getFilterUUID() - */ - private void showDeviceScanningDialog(final UUID filter) { - final ScannerFragment dialog = ScannerFragment.getInstance(filter); - dialog.show(getSupportFragmentManager(), "scan_fragment"); - } - - private void ensureBLESupported() { - if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { - Toast.makeText(this, R.string.no_ble, Toast.LENGTH_LONG).show(); - finish(); - } - } - - protected boolean isBLEEnabled() { - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - return adapter != null && adapter.isEnabled(); - } - - protected void showBLEDialog() { - final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult(enableIntent, REQUEST_ENABLE_BT); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/IDeviceLogger.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/IDeviceLogger.java deleted file mode 100644 index a2285473..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/profile/multiconnect/IDeviceLogger.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2016, 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.profile.multiconnect; - -import android.bluetooth.BluetoothDevice; - -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; - -public interface IDeviceLogger { - /** - * Logs the given message with given log level into the device's log session. - * @param device the target device - * @param level the log level - * @param message the message to be logged - */ - void log(@NonNull final BluetoothDevice device, final int level, final String message); - - /** - * Logs the given message with given log level into the device's log session. - * @param device the target device - * @param level the log level - * @param messageRes string resource id - * @param params additional (optional) parameters used to fill the message - */ - void log(@NonNull final BluetoothDevice device, final int level, @StringRes final int messageRes, final Object... params); -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/DeviceAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/DeviceAdapter.java deleted file mode 100644 index 741ce79d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/DeviceAdapter.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2016, 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.proximity; - -import android.bluetooth.BluetoothDevice; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ProgressBar; -import android.widget.TextView; - -import java.util.List; - -import no.nordicsemi.android.nrftoolbox.R; - -class DeviceAdapter extends RecyclerView.Adapter { - private final ProximityService.ProximityBinder service; - private final List devices; - - DeviceAdapter(@NonNull final ProximityService.ProximityBinder binder) { - service = binder; - devices = service.getManagedDevices(); - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { - final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_feature_proximity_item, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { - holder.bind(devices.get(position)); - } - - @Override - public int getItemCount() { - return devices.size(); - } - - void onDeviceAdded(final BluetoothDevice device) { - final int position = devices.indexOf(device); - if (position == -1) { - notifyItemInserted(devices.size() - 1); - } else { - // This may happen when Bluetooth adapter was switched off and on again - // while there were devices on the list. - notifyItemChanged(position); - } - } - - void onDeviceRemoved(final BluetoothDevice device) { - notifyDataSetChanged(); // we don't have position of the removed device here - } - - void onDeviceStateChanged(final BluetoothDevice device) { - final int position = devices.indexOf(device); - if (position >= 0) - notifyItemChanged(position); - } - - void onBatteryValueReceived(final BluetoothDevice device) { - final int position = devices.indexOf(device); - if (position >= 0) - notifyItemChanged(position); - } - - class ViewHolder extends RecyclerView.ViewHolder { - private TextView nameView; - private TextView addressView; - private TextView batteryView; - private ImageButton actionButton; - private ProgressBar progress; - - ViewHolder(final View itemView) { - super(itemView); - - nameView = itemView.findViewById(R.id.name); - addressView = itemView.findViewById(R.id.address); - batteryView = itemView.findViewById(R.id.battery); - actionButton = itemView.findViewById(R.id.action_find_silent); - progress = itemView.findViewById(R.id.progress); - - // Configure FIND / SILENT button - actionButton.setOnClickListener(v -> { - final int position = getAdapterPosition(); - final BluetoothDevice device = devices.get(position); - service.toggleImmediateAlert(device); - }); - - // Configure Disconnect button - itemView.findViewById(R.id.action_disconnect).setOnClickListener(v -> { - final int position = getAdapterPosition(); - final BluetoothDevice device = devices.get(position); - service.disconnect(device); - // The device might have not been connected, so there will be no callback - onDeviceRemoved(device); - }); - } - - private void bind(@NonNull final BluetoothDevice device) { - final boolean ready = service.isReady(device); - - String name = device.getName(); - if (TextUtils.isEmpty(name)) - name = nameView.getResources().getString(R.string.proximity_default_device_name); - nameView.setText(name); - addressView.setText(device.getAddress()); - - final boolean on = service.isImmediateAlertOn(device); - actionButton.setImageResource(on ? R.drawable.ic_stat_notify_proximity_silent : R.drawable.ic_stat_notify_proximity_find); - actionButton.setVisibility(ready ? View.VISIBLE : View.GONE); - progress.setVisibility(ready ? View.GONE : View.VISIBLE); - - final Integer batteryValue = service.getBatteryLevel(device); - if (batteryValue != null) { - batteryView.getCompoundDrawables()[0 /*left*/].setLevel(batteryValue); - batteryView.setVisibility(View.VISIBLE); - batteryView.setText(batteryView.getResources().getString(R.string.battery, batteryValue)); - batteryView.setAlpha(ready ? 1.0f : 0.5f); - } else { - batteryView.setVisibility(View.GONE); - } - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/LinkLossDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/LinkLossDialogFragment.java deleted file mode 100644 index 1d85e30f..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/LinkLossDialogFragment.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2015, 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.proximity; - -import android.app.Dialog; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.appcompat.app.AlertDialog; - -import no.nordicsemi.android.nrftoolbox.R; - -public class LinkLossDialogFragment extends DialogFragment { - private static final String ARG_NAME = "name"; - - private String name; - - public static LinkLossDialogFragment getInstance(@NonNull final String name) { - final LinkLossDialogFragment fragment = new LinkLossDialogFragment(); - - final Bundle args = new Bundle(); - args.putString(ARG_NAME, name); - fragment.setArguments(args); - - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - name = requireArguments().getString(ARG_NAME); - } - - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(requireContext()) - .setTitle(getString(R.string.app_name)) - .setMessage(getString(R.string.proximity_notification_link_loss_alert, name)) - .setPositiveButton(R.string.ok, null) - .create(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java deleted file mode 100644 index 4fc2218b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityActivity.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (c) 2015, 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.proximity; - -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileService; -import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileServiceReadyActivity; -import no.nordicsemi.android.nrftoolbox.widget.DividerItemDecoration; - -public class ProximityActivity extends BleMulticonnectProfileServiceReadyActivity { - private RecyclerView devicesView; - private DeviceAdapter adapter; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_proximity); - setGUI(); - } - - @Override - protected void onInitialize(final Bundle savedInstanceState) { - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, makeIntentFilter()); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); - } - - private void setGUI() { - final RecyclerView recyclerView = devicesView = findViewById(android.R.id.list); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST)); - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.proximity_feature_title; - } - - @Override - protected void onServiceBound(final ProximityService.ProximityBinder binder) { - devicesView.setAdapter(adapter = new DeviceAdapter(binder)); - } - - @Override - protected void onServiceUnbound() { - devicesView.setAdapter(adapter = null); - } - - @Override - protected Class getServiceClass() { - return ProximityService.class; - } - - @Override - protected int getAboutTextId() { - return R.string.proximity_about_text; - } - - @Override - protected UUID getFilterUUID() { - return ProximityManager.LINK_LOSS_SERVICE_UUID; - } - - @Override - public void onDeviceConnecting(@NonNull final BluetoothDevice device) { - if (adapter != null) - adapter.onDeviceAdded(device); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - if (adapter != null) - adapter.onDeviceStateChanged(device); - } - - @Override - public void onDeviceReady(@NonNull final BluetoothDevice device) { - if (adapter != null) - adapter.onDeviceStateChanged(device); - } - - @Override - public void onDeviceDisconnecting(@NonNull final BluetoothDevice device) { - if (adapter != null) - adapter.onDeviceStateChanged(device); - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - if (adapter != null) - adapter.onDeviceRemoved(device); - } - - @Override - public void onDeviceNotSupported(@NonNull final BluetoothDevice device) { - super.onDeviceNotSupported(device); - if (adapter != null) - adapter.onDeviceRemoved(device); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - if (adapter != null) - adapter.onDeviceStateChanged(device); - - // The link loss may also be called when Bluetooth adapter was disabled - if (BluetoothAdapter.getDefaultAdapter().isEnabled()) - showLinkLossDialog(device.getName()); - } - - @SuppressWarnings("unused") - private void onBatteryLevelChanged(final BluetoothDevice device, final int batteryLevel) { - if (adapter != null) - adapter.onBatteryValueReceived(device); // Value will be obtained from the service - } - - @SuppressWarnings("unused") - private void onRemoteAlarmSwitched(final BluetoothDevice device, final boolean on) { - if (adapter != null) - adapter.onDeviceStateChanged(device); // Value will be obtained from the service - } - - private void showLinkLossDialog(final String name) { - try { - final LinkLossDialogFragment dialog = LinkLossDialogFragment.getInstance(name); - dialog.show(getSupportFragmentManager(), "scan_fragment"); - } catch (final Exception e) { - // the activity must have been destroyed - } - } - - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - final BluetoothDevice device = intent.getParcelableExtra(ProximityService.EXTRA_DEVICE); - - if (ProximityService.BROADCAST_BATTERY_LEVEL.equals(action)) { - final int batteryLevel = intent.getIntExtra(ProximityService.EXTRA_BATTERY_LEVEL, 0); - // Update GUI - onBatteryLevelChanged(device, batteryLevel); - } else if (ProximityService.BROADCAST_ALARM_SWITCHED.equals(action)) { - final boolean on = intent.getBooleanExtra(ProximityService.EXTRA_ALARM_STATE, false); - // Update GUI - onRemoteAlarmSwitched(device, on); - } - } - }; - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ProximityService.BROADCAST_BATTERY_LEVEL); - intentFilter.addAction(ProximityService.BROADCAST_ALARM_SWITCHED); - return intentFilter; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManager.java deleted file mode 100644 index a1e1f791..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManager.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (c) 2015, 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.proximity; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattServer; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import androidx.annotation.NonNull; -import android.util.Log; - -import java.util.UUID; - -import no.nordicsemi.android.ble.callback.FailCallback; -import no.nordicsemi.android.ble.common.callback.alert.AlertLevelDataCallback; -import no.nordicsemi.android.ble.common.data.alert.AlertLevelData; -import no.nordicsemi.android.ble.error.GattError; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.AlertLevelParser; - -@SuppressWarnings("WeakerAccess") -class ProximityManager extends BatteryManager { - /** Link Loss service UUID. */ - final static UUID LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb"); - /** Immediate Alert service UUID. */ - final static UUID IMMEDIATE_ALERT_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb"); - /** Alert Level characteristic UUID. */ - final static UUID ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb"); - - // Client characteristics. - private BluetoothGattCharacteristic alertLevelCharacteristic, linkLossCharacteristic; - // Server characteristics. - private BluetoothGattCharacteristic localAlertLevelCharacteristic; - /** A flag indicating whether the alarm on the connected proximity tag has been activated. */ - private boolean alertOn; - - ProximityManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new ProximityManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ - private class ProximityManagerGattCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - super.initialize(); - // This callback will be called whenever local Alert Level char is written - // by a connected proximity tag. - setWriteCallback(localAlertLevelCharacteristic) - .with(new AlertLevelDataCallback() { - @Override - public void onAlertLevelChanged(@NonNull final BluetoothDevice device, final int level) { - mCallbacks.onLocalAlarmSwitched(device, level != ALERT_NONE); - } - }); - // After connection, set the Link Loss behaviour on the tag. - writeCharacteristic(linkLossCharacteristic, AlertLevelData.highAlert()) - .done(device -> log(Log.INFO, "Link loss alert level set")) - .fail((device, status) -> log(Log.WARN, "Failed to set link loss level: " + status)) - .enqueue(); - } - - @Override - protected void onServerReady(@NonNull final BluetoothGattServer server) { - final BluetoothGattService immediateAlertService = server.getService(IMMEDIATE_ALERT_SERVICE_UUID); - if (immediateAlertService != null) { - localAlertLevelCharacteristic = immediateAlertService.getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID); - } - } - - @Override - protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService llService = gatt.getService(LINK_LOSS_SERVICE_UUID); - if (llService != null) { - linkLossCharacteristic = llService.getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID); - } - return linkLossCharacteristic != null; - } - - @Override - protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) { - super.isOptionalServiceSupported(gatt); - final BluetoothGattService iaService = gatt.getService(IMMEDIATE_ALERT_SERVICE_UUID); - if (iaService != null) { - alertLevelCharacteristic = iaService.getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID); - } - return alertLevelCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - super.onDeviceDisconnected(); - alertLevelCharacteristic = null; - linkLossCharacteristic = null; - localAlertLevelCharacteristic = null; - // Reset the alert flag - alertOn = false; - } - } - - /** - * Toggles the immediate alert on the target device. - */ - public void toggleImmediateAlert() { - writeImmediateAlert(!alertOn); - } - - /** - * Writes the HIGH ALERT or NO ALERT command to the target device. - * - * @param on true to enable the alarm on proximity tag, false to disable it. - */ - public void writeImmediateAlert(final boolean on) { - if (!isConnected()) - return; - - writeCharacteristic(alertLevelCharacteristic, on ? AlertLevelData.highAlert() : AlertLevelData.noAlert()) - .before(device -> log(Log.VERBOSE, - on ? "Setting alarm to HIGH..." : "Disabling alarm...")) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, - "\"" + AlertLevelParser.parse(data) + "\" sent")) - .done(device -> { - alertOn = on; - mCallbacks.onRemoteAlarmSwitched(device, on); - }) - .fail((device, status) -> log(Log.WARN, - status == FailCallback.REASON_NULL_ATTRIBUTE ? - "Alert Level characteristic not found" : - GattError.parse(status))) - .enqueue(); - } - - /** - * Returns true if the alert has been enabled on the proximity tag, false otherwise. - */ - boolean isAlertEnabled() { - return alertOn; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java deleted file mode 100644 index 5d2964fa..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityManagerCallbacks.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2015, 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.proximity; - -import android.bluetooth.BluetoothDevice; -import androidx.annotation.NonNull; - -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -interface ProximityManagerCallbacks extends BatteryManagerCallbacks { - void onRemoteAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on); - - void onLocalAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on); -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManager.java deleted file mode 100644 index b8af272e..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityServerManager.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2015, 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.proximity; - -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; -import no.nordicsemi.android.ble.BleServerManager; -import no.nordicsemi.android.ble.common.data.alert.AlertLevelData; - -class ProximityServerManager extends BleServerManager { - - ProximityServerManager(@NonNull final Context context) { - super(context); - } - - @Override - public void log(final int priority, @NonNull final String message) { - Log.println(priority, "BleManager", message); - } - - @NonNull - @Override - protected List initializeServer() { - final List services = new ArrayList<>(); - services.add( - service(ProximityManager.IMMEDIATE_ALERT_SERVICE_UUID, - characteristic(ProximityManager.ALERT_LEVEL_CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, - BluetoothGattCharacteristic.PERMISSION_WRITE)) - ); - services.add( - service(ProximityManager.LINK_LOSS_SERVICE_UUID, - characteristic(ProximityManager.ALERT_LEVEL_CHARACTERISTIC_UUID, - BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_READ, - BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ, - AlertLevelData.highAlert())) - ); - return services; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java deleted file mode 100644 index 34a9db25..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/proximity/ProximityService.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright (c) 2015, 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.proximity; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Build; -import android.text.TextUtils; -import android.util.Log; - -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.ble.observer.ServerObserver; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; -import no.nordicsemi.android.nrftoolbox.profile.multiconnect.BleMulticonnectProfileService; - -public class ProximityService extends BleMulticonnectProfileService implements ProximityManagerCallbacks, ServerObserver { - @SuppressWarnings("unused") - private static final String TAG = "ProximityService"; - - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - - public static final String BROADCAST_ALARM_SWITCHED = "no.nordicsemi.android.nrftoolbox.BROADCAST_ALARM_SWITCHED"; - public static final String EXTRA_ALARM_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_ALARM_STATE"; - - private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_DISCONNECT"; - private final static String ACTION_FIND = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_FIND"; - private final static String ACTION_SILENT = "no.nordicsemi.android.nrftoolbox.proximity.ACTION_SILENT"; - - private final static String PROXIMITY_GROUP_ID = "proximity_connected_tags"; - private final static int NOTIFICATION_ID = 1000; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; - private final static int FIND_REQ = 2; - private final static int SILENT_REQ = 3; - - private final ProximityBinder binder = new ProximityBinder(); - private ProximityServerManager serverManager; - private MediaPlayer mediaPlayer; - private int originalVolume; - /** - * When a device starts an alarm on the phone it is added to this list. - * Alarm is disabled when this list is empty. - */ - private List devicesWithAlarm; - - /** - * This local binder is an interface for the bonded activity to operate with the proximity - * sensor. - */ - class ProximityBinder extends LocalBinder { - /** - * Toggles the Immediate Alert on given remote device. - * - * @param device the connected device. - */ - void toggleImmediateAlert(final BluetoothDevice device) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - manager.toggleImmediateAlert(); - } - - /** - * Returns the current alarm state on given device. This value is not read from the device, - * it's just the last value written to it (initially false). - * - * @param device the connected device. - * @return True if alarm has been enabled, false if disabled. - */ - boolean isImmediateAlertOn(final BluetoothDevice device) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - return manager.isAlertEnabled(); - } - - /** - * Returns the last received battery level value. - * - * @param device the device of which battery level should be returned. - * @return Battery value or null if no value was received or Battery Level characteristic - * was not found, or the device is disconnected. - */ - Integer getBatteryLevel(final BluetoothDevice device) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - return manager.getBatteryLevel(); - } - } - - @Override - protected LocalBinder getBinder() { - return binder; - } - - @Override - protected LoggableBleManager initializeManager() { - final ProximityManager manager = new ProximityManager(this); - manager.useServer(serverManager); - return manager; - } - - @Override - protected boolean shouldAutoConnect() { - return true; - } - - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing - * Disconnect action button on the notification. - */ - private final BroadcastReceiver disconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); - binder.log(device, LogContract.Log.Level.INFO, "[Notification] DISCONNECT action pressed"); - binder.disconnect(device); - } - }; - - /** - * This broadcast receiver listens for {@link #ACTION_FIND} or {@link #ACTION_SILENT} that may - * be fired by pressing Find me action button on the notification. - */ - private final BroadcastReceiver toggleAlarmActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final BluetoothDevice device = intent.getParcelableExtra(EXTRA_DEVICE); - switch (intent.getAction()) { - case ACTION_FIND: - binder.log(device, LogContract.Log.Level.INFO, "[Notification] FIND action pressed"); - break; - case ACTION_SILENT: - binder.log(device, LogContract.Log.Level.INFO, "[Notification] SILENT action pressed"); - break; - } - binder.toggleImmediateAlert(device); - } - }; - - @Override - protected void onServiceCreated() { - serverManager = new ProximityServerManager(this); - serverManager.setServerObserver(this); - - initializeAlarm(); - - registerReceiver(disconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT)); - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_FIND); - filter.addAction(ACTION_SILENT); - registerReceiver(toggleAlarmActionBroadcastReceiver, filter); - } - - @Override - public void onServiceStopped() { - cancelNotifications(); - - // Close the GATT server. If it hasn't been opened this method does nothing - serverManager.close(); - serverManager = null; - - releaseAlarm(); - - unregisterReceiver(disconnectActionBroadcastReceiver); - unregisterReceiver(toggleAlarmActionBroadcastReceiver); - - super.onServiceStopped(); - } - - @Override - protected void onBluetoothEnabled() { - // First, open the server. onServerReady() will be called when all services were added. - serverManager.open(); - } - - @Override - public void onServerReady() { - // This will start reconnecting to devices that will previously connected. - super.onBluetoothEnabled(); - } - - @Override - protected void onBluetoothDisabled() { - super.onBluetoothDisabled(); - // Close the GATT server - serverManager.close(); - } - - @Override - protected void onRebind() { - // When the activity rebinds to the service, remove the notification - cancelNotifications(); - - // This method will read the Battery Level value from each connected device, if possible - // and then try to enable battery notifications (if it has NOTIFY property). - // If the Battery Level characteristic has only the NOTIFY property, it will only try to - // enable notifications. - for (final BluetoothDevice device : getManagedDevices()) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - manager.readBatteryLevelCharacteristic(); - manager.enableBatteryLevelCharacteristicNotifications(); - } - } - - @Override - public void onUnbind() { - // When we are connected, but the application is not open, we are not really interested - // in battery level notifications. But we will still be receiving other values, if enabled. - for (final BluetoothDevice device : getManagedDevices()) { - final ProximityManager manager = (ProximityManager) getBleManager(device); - manager.disableBatteryLevelCharacteristicNotifications(); - } - - createBackgroundNotification(); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - super.onDeviceConnected(device); - - if (!bound) { - createBackgroundNotification(); - } - } - - @Override - public void onDeviceConnectedToServer(@NonNull final BluetoothDevice device) { - binder.log(Log.INFO, device.getAddress() + " connected to server"); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - stopAlarm(device); - super.onLinkLossOccurred(device); - - if (!bound) { - createBackgroundNotification(); - if (BluetoothAdapter.getDefaultAdapter().isEnabled()) - createLinkLossNotification(device); - else - cancelNotification(device); - } - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - stopAlarm(device); - super.onDeviceDisconnected(device); - - if (!bound) { - cancelNotification(device); - createBackgroundNotification(); - } - } - - @Override - public void onDeviceDisconnectedFromServer(@NonNull final BluetoothDevice device) { - binder.log(Log.INFO, device.getAddress() + " disconnected from server"); - } - - @Override - public void onRemoteAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on) { - final Intent broadcast = new Intent(BROADCAST_ALARM_SWITCHED); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_ALARM_STATE, on); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - if (!bound) { - createBackgroundNotification(); - } - } - - @Override - public void onLocalAlarmSwitched(@NonNull final BluetoothDevice device, final boolean on) { - if (on) - playAlarm(device); - else - stopAlarm(device); - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, device); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, batteryLevel); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - private void createBackgroundNotification() { - final List connectedDevices = getConnectedDevices(); - for (final BluetoothDevice device : connectedDevices) { - createNotificationForConnectedDevice(device); - } - createSummaryNotification(); - } - - private void createSummaryNotification() { - final NotificationCompat.Builder builder = getNotificationBuilder(); - builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); - builder.setShowWhen(false).setDefaults(0); - // An ongoing notification will not be shown on Android Wear. - builder.setOngoing(true); - builder.setGroup(PROXIMITY_GROUP_ID).setGroupSummary(true); - builder.setContentTitle(getString(R.string.app_name)); - - final List managedDevices = getManagedDevices(); - final List connectedDevices = getConnectedDevices(); - if (connectedDevices.isEmpty()) { - // No connected devices - final int numberOfManagedDevices = managedDevices.size(); - if (numberOfManagedDevices == 1) { - final String name = getDeviceName(managedDevices.get(0)); - // We don't use plurals here, as we only have the default language and 'one' is not - // in every language (versions differ in %d or %s) and throw an exception in e.g. in Chinese. - builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name)); - } else { - builder.setContentText(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfManagedDevices)); - } - } else { - // There are some proximity tags connected - final StringBuilder text = new StringBuilder(); - - final int numberOfConnectedDevices = connectedDevices.size(); - if (numberOfConnectedDevices == 1) { - final String name = getDeviceName(connectedDevices.get(0)); - text.append(getString(R.string.proximity_notification_summary_text_name, name)); - } else { - text.append(getString(R.string.proximity_notification_summary_text_number, numberOfConnectedDevices)); - } - - // If there are some disconnected devices, also print them - final int numberOfDisconnectedDevices = managedDevices.size() - numberOfConnectedDevices; - if (numberOfDisconnectedDevices == 1) { - text.append(", "); - // Find the single disconnected device to get its name - for (final BluetoothDevice device : managedDevices) { - if (!isConnected(device)) { - final String name = getDeviceName(device); - text.append(getString(R.string.proximity_notification_text_nothing_connected_one_disconnected, name)); - break; - } - } - } else if (numberOfDisconnectedDevices > 1) { - text.append(", "); - // If there are more, just write number of them - text.append(getString(R.string.proximity_notification_text_nothing_connected_number_disconnected, numberOfDisconnectedDevices)); - } - text.append("."); - builder.setContentText(text); - } - - final Notification notification = builder.build(); - final NotificationManagerCompat nm = NotificationManagerCompat.from(this); - nm.notify(NOTIFICATION_ID, notification); - } - - /** - * Creates the notification for given connected device. - * Adds 3 action buttons: DISCONNECT, FIND and SILENT which perform given action on the device. - */ - private void createNotificationForConnectedDevice(final BluetoothDevice device) { - final NotificationCompat.Builder builder = getNotificationBuilder(); - builder.setColor(ContextCompat.getColor(this, R.color.actionBarColorDark)); - builder.setGroup(PROXIMITY_GROUP_ID).setDefaults(0); - // An ongoing notification will not be shown on Android Wear. - builder.setOngoing(true); - builder.setContentTitle(getString(R.string.proximity_notification_text, getDeviceName(device))); - - // Add DISCONNECT action - final Intent disconnect = new Intent(ACTION_DISCONNECT); - disconnect.putExtra(EXTRA_DEVICE, device); - final PendingIntent disconnectAction = - PendingIntent.getBroadcast(this, DISCONNECT_REQ + device.hashCode(), - disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.proximity_action_disconnect), disconnectAction)); - // This will keep the same order of notification even after an action was clicked on one of them. - builder.setSortKey(getDeviceName(device) + device.getAddress()); - - // Add FIND or SILENT action - final ProximityManager manager = (ProximityManager) getBleManager(device); - if (manager.isAlertEnabled()) { - final Intent silentAllIntent = new Intent(ACTION_SILENT); - silentAllIntent.putExtra(EXTRA_DEVICE, device); - final PendingIntent silentAction = - PendingIntent.getBroadcast(this, SILENT_REQ + device.hashCode(), - silentAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_silent, getString(R.string.proximity_action_silent), silentAction)); - } else { - final Intent findAllIntent = new Intent(ACTION_FIND); - findAllIntent.putExtra(EXTRA_DEVICE, device); - final PendingIntent findAction = - PendingIntent.getBroadcast(this, FIND_REQ + device.hashCode(), - findAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_stat_notify_proximity_find, getString(R.string.proximity_action_find), findAction)); - } - - final Notification notification = builder.build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - } - - /** - * Creates a notification showing information about a device that got disconnected. - */ - private void createLinkLossNotification(final BluetoothDevice device) { - final NotificationCompat.Builder builder = getNotificationBuilder(); - builder.setColor(ContextCompat.getColor(this, R.color.orange)); - - final Uri notificationUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); - // Make sure the sound is played even in DND mode - builder.setSound(notificationUri, AudioManager.STREAM_ALARM); - builder.setPriority(NotificationCompat.PRIORITY_HIGH); - builder.setCategory(NotificationCompat.CATEGORY_ALARM); - builder.setShowWhen(true); - // An ongoing notification would not be shown on Android Wear. - builder.setOngoing(false); - // This notification is to be shown not in a group - - final String name = getDeviceName(device); - builder.setContentTitle(getString(R.string.proximity_notification_link_loss_alert, name)); - builder.setTicker(getString(R.string.proximity_notification_link_loss_alert, name)); - - final Notification notification = builder.build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(device.getAddress(), NOTIFICATION_ID, notification); - } - } - - private NotificationCompat.Builder getNotificationBuilder() { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, ProximityActivity.class); - - // Both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); - - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.PROXIMITY_WARNINGS_CHANNEL); - builder.setContentIntent(pendingIntent).setAutoCancel(false); - builder.setSmallIcon(R.drawable.ic_stat_notify_proximity); - return builder; - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotifications() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - stopForeground(true); - } else { - nm.cancel(NOTIFICATION_ID); - } - - final List managedDevices = getManagedDevices(); - for (final BluetoothDevice device : managedDevices) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - stopForeground(true); - } else { - nm.cancel(device.getAddress(), NOTIFICATION_ID); - } - } - } - - /** - * Cancels the existing notification for given device. If there is no active notification this method does nothing - */ - private void cancelNotification(final BluetoothDevice device) { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - stopForeground(true); - } else { - nm.cancel(device.getAddress(), NOTIFICATION_ID); - } - } - - private void initializeAlarm() { - devicesWithAlarm = new LinkedList<>(); - mediaPlayer = new MediaPlayer(); - mediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); - mediaPlayer.setLooping(true); - mediaPlayer.setVolume(1.0f, 1.0f); - try { - mediaPlayer.setDataSource(this, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)); - } catch (final IOException e) { - Log.e(TAG, "Initialize Alarm failed: ", e); - } - } - - private void releaseAlarm() { - mediaPlayer.release(); - mediaPlayer = null; - } - - private void playAlarm(final BluetoothDevice device) { - final boolean alarmPlaying = !devicesWithAlarm.isEmpty(); - if (!devicesWithAlarm.contains(device)) - devicesWithAlarm.add(device); - - if (!alarmPlaying) { - // Save the current alarm volume and set it to max - final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - originalVolume = am.getStreamVolume(AudioManager.STREAM_ALARM); - am.setStreamVolume(AudioManager.STREAM_ALARM, am.getStreamMaxVolume(AudioManager.STREAM_ALARM), AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); - try { - mediaPlayer.prepare(); - mediaPlayer.start(); - } catch (final IOException e) { - Log.e(TAG, "Prepare Alarm failed: ", e); - } - } - } - - private void stopAlarm(final BluetoothDevice device) { - devicesWithAlarm.remove(device); - if (devicesWithAlarm.isEmpty() && mediaPlayer.isPlaying()) { - mediaPlayer.stop(); - // Restore original volume - final AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - am.setStreamVolume(AudioManager.STREAM_ALARM, originalVolume, 0); - } - } - - private String getDeviceName(final BluetoothDevice device) { - String name = device.getName(); - if (TextUtils.isEmpty(name)) - name = getString(R.string.proximity_default_device_name); - return name; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCActivity.java deleted file mode 100644 index dbbbcadc..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCActivity.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (c) 2015, 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.rsc; - -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; - -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.view.Menu; -import android.widget.TextView; - -import java.util.Locale; -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; -import no.nordicsemi.android.nrftoolbox.rsc.settings.SettingsActivity; -import no.nordicsemi.android.nrftoolbox.rsc.settings.SettingsFragment; - -public class RSCActivity extends BleProfileServiceReadyActivity { - private TextView speedView; - private TextView speedUnitView; - private TextView cadenceView; - private TextView distanceView; - private TextView distanceUnitView; - private TextView totalDistanceView; - private TextView totalDistanceUnitView; - private TextView stridesCountView; - private TextView activityView; - private TextView batteryLevelView; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_rsc); - setGui(); - } - - @Override - protected void onInitialize(final Bundle savedInstanceState) { - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, makeIntentFilter()); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); - } - - private void setGui() { - speedView = findViewById(R.id.speed); - speedUnitView = findViewById(R.id.speed_unit); - cadenceView = findViewById(R.id.cadence); - distanceView = findViewById(R.id.distance); - distanceUnitView = findViewById(R.id.distance_unit); - totalDistanceView = findViewById(R.id.total_distance); - totalDistanceUnitView = findViewById(R.id.total_distance_unit); - stridesCountView = findViewById(R.id.strides); - activityView = findViewById(R.id.activity); - batteryLevelView = findViewById(R.id.battery); - } - - @Override - protected void onResume() { - super.onResume(); - setDefaultUI(); - } - - @Override - protected void setDefaultUI() { - speedView.setText(R.string.not_available_value); - cadenceView.setText(R.string.not_available_value); - distanceView.setText(R.string.not_available_value); - totalDistanceView.setText(R.string.not_available_value); - stridesCountView.setText(R.string.not_available_value); - activityView.setText(R.string.not_available); - batteryLevelView.setText(R.string.not_available); - - setUnits(); - } - - private void setUnits() { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT))); - - switch (unit) { - case SettingsFragment.SETTINGS_UNIT_M_S: // [m/s] - speedUnitView.setText(R.string.rsc_speed_unit_m_s); - distanceUnitView.setText(R.string.rsc_distance_unit_m); - totalDistanceUnitView.setText(R.string.rsc_total_distance_unit_km); - break; - case SettingsFragment.SETTINGS_UNIT_KM_H: // [km/h] - speedUnitView.setText(R.string.rsc_speed_unit_km_h); - distanceUnitView.setText(R.string.rsc_distance_unit_m); - totalDistanceUnitView.setText(R.string.rsc_total_distance_unit_km); - break; - case SettingsFragment.SETTINGS_UNIT_MPH: // [mph] - speedUnitView.setText(R.string.rsc_speed_unit_mph); - distanceUnitView.setText(R.string.rsc_distance_unit_yd); - totalDistanceUnitView.setText(R.string.rsc_total_distance_unit_mile); - break; - } - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.rsc_feature_title; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.rsc_default_name; - } - - @Override - protected int getAboutTextId() { - return R.string.rsc_about_text; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.settings_and_about, menu); - return true; - } - - @Override - protected boolean onOptionsItemSelected(final int itemId) { - switch (itemId) { - case R.id.action_settings: - final Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - break; - } - return true; - } - - @Override - protected Class getServiceClass() { - return RSCService.class; - } - - @Override - protected UUID getFilterUUID() { - return RSCManager.RUNNING_SPEED_AND_CADENCE_SERVICE_UUID; - } - - @Override - protected void onServiceBound(final RSCService.RSCBinder binder) { - // not used - } - - @Override - protected void onServiceUnbound() { - // not used - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // not used - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - batteryLevelView.setText(R.string.not_available); - } - - private void onMeasurementReceived(float speed, int cadence, long totalDistance, final boolean running) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT))); - - switch (unit) { - case SettingsFragment.SETTINGS_UNIT_KM_H: - speed = speed * 3.6f; - // pass through intended - case SettingsFragment.SETTINGS_UNIT_M_S: - if (totalDistance == -1) { - totalDistanceView.setText(R.string.not_available); - totalDistanceUnitView.setText(null); - } else { - totalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1000.0f)); // 1 km in m - totalDistanceUnitView.setText(R.string.rsc_total_distance_unit_km); - } - break; - case SettingsFragment.SETTINGS_UNIT_MPH: - speed = speed * 2.2369f; - if (totalDistance == -1) { - totalDistanceView.setText(R.string.not_available); - totalDistanceUnitView.setText(null); - } else { - totalDistanceView.setText(String.format(Locale.US, "%.2f", totalDistance / 1609.31f)); // 1 mile in m - totalDistanceUnitView.setText(R.string.rsc_total_distance_unit_mile); - } - break; - } - - speedView.setText(String.format(Locale.US, "%.1f", speed)); - cadenceView.setText(String.format(Locale.US, "%d", cadence)); - activityView.setText(running ? R.string.rsc_running : R.string.rsc_walking); - } - - private void onStripesUpdate(final long distance, final int strides) { - if (distance == -1) { - distanceView.setText(R.string.not_available); - distanceUnitView.setText(R.string.rsc_distance_unit_m); - } else { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - final int unit = Integer.parseInt(preferences.getString(SettingsFragment.SETTINGS_UNIT, String.valueOf(SettingsFragment.SETTINGS_UNIT_DEFAULT))); - - switch (unit) { - case SettingsFragment.SETTINGS_UNIT_KM_H: - case SettingsFragment.SETTINGS_UNIT_M_S: - if (distance < 100000L) { // 1 km in cm - distanceView.setText(String.format(Locale.US, "%.1f", distance / 100.0f)); - distanceUnitView.setText(R.string.rsc_distance_unit_m); - } else { - distanceView.setText(String.format(Locale.US, "%.2f", distance / 100000.0f)); - distanceUnitView.setText(R.string.rsc_distance_unit_km); - } - break; - case SettingsFragment.SETTINGS_UNIT_MPH: - if (distance < 160931L) { // 1 mile in cm - distanceView.setText(String.format(Locale.US, "%.1f", distance / 91.4392f)); - distanceUnitView.setText(R.string.rsc_distance_unit_yd); - } else { - distanceView.setText(String.format(Locale.US, "%.2f", distance / 160931.23f)); - distanceUnitView.setText(R.string.rsc_distance_unit_mile); - } - break; - } - } - - stridesCountView.setText(String.valueOf(strides)); - } - - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - - if (RSCService.BROADCAST_RSC_MEASUREMENT.equals(action)) { - final float speed = intent.getFloatExtra(RSCService.EXTRA_SPEED, 0.0f); - final int cadence = intent.getIntExtra(RSCService.EXTRA_CADENCE, 0); - final long totalDistance = intent.getLongExtra(RSCService.EXTRA_TOTAL_DISTANCE, -1); - final boolean running = intent.getBooleanExtra(RSCService.EXTRA_ACTIVITY, false); - // Update GUI - onMeasurementReceived(speed, cadence, totalDistance, running); - } else if (RSCService.BROADCAST_STRIDES_UPDATE.equals(action)) { - final int strides = intent.getIntExtra(RSCService.EXTRA_STRIDES, 0); - final long distance = intent.getLongExtra(RSCService.EXTRA_DISTANCE, -1); - // Update GUI - onStripesUpdate(distance, strides); - } - } - }; - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(RSCService.BROADCAST_RSC_MEASUREMENT); - intentFilter.addAction(RSCService.BROADCAST_STRIDES_UPDATE); - return intentFilter; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManager.java deleted file mode 100644 index 90702d89..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManager.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2015, 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.rsc; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.UUID; - -import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.RSCMeasurementParser; - -public class RSCManager extends BatteryManager { - /** Running Speed and Cadence Measurement service UUID */ - static final UUID RUNNING_SPEED_AND_CADENCE_SERVICE_UUID = UUID.fromString("00001814-0000-1000-8000-00805f9b34fb"); - /** Running Speed and Cadence Measurement characteristic UUID */ - private static final UUID RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-0000-1000-8000-00805f9b34fb"); - - private BluetoothGattCharacteristic rscMeasurementCharacteristic; - - RSCManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new RSCManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ - private class RSCManagerGattCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - super.initialize(); - setNotificationCallback(rscMeasurementCharacteristic) - .with(new RunningSpeedAndCadenceMeasurementDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, "\"" + RSCMeasurementParser.parse(data) + "\" received"); - super.onDataReceived(device, data); - } - - @Override - public void onRSCMeasurementReceived(@NonNull final BluetoothDevice device, final boolean running, - final float instantaneousSpeed, final int instantaneousCadence, - @Nullable final Integer strideLength, - @Nullable final Long totalDistance) { - mCallbacks.onRSCMeasurementReceived(device, running, instantaneousSpeed, - instantaneousCadence, strideLength, totalDistance); - } - }); - enableNotifications(rscMeasurementCharacteristic).enqueue(); - } - - @Override - public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(RUNNING_SPEED_AND_CADENCE_SERVICE_UUID); - if (service != null) { - rscMeasurementCharacteristic = service.getCharacteristic(RSC_MEASUREMENT_CHARACTERISTIC_UUID); - } - return rscMeasurementCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - super.onDeviceDisconnected(); - rscMeasurementCharacteristic = null; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManagerCallbacks.java deleted file mode 100644 index 1a06cd26..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCManagerCallbacks.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2015, 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.rsc; - -import no.nordicsemi.android.ble.common.profile.rsc.RunningSpeedAndCadenceMeasurementCallback; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; - -interface RSCManagerCallbacks extends BatteryManagerCallbacks, RunningSpeedAndCadenceMeasurementCallback { - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java deleted file mode 100644 index b21bbe52..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/RSCService.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright (c) 2015, 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.rsc; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.os.Handler; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -public class RSCService extends BleProfileService implements RSCManagerCallbacks { - @SuppressWarnings("unused") - private static final String TAG = "RSCService"; - - public static final String BROADCAST_RSC_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_RSC_MEASUREMENT"; - public static final String EXTRA_SPEED = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_SPEED"; - public static final String EXTRA_CADENCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_CADENCE"; - public static final String EXTRA_STRIDE_LENGTH = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDE_LENGTH"; - public static final String EXTRA_TOTAL_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_TOTAL_DISTANCE"; - public static final String EXTRA_ACTIVITY = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_ACTIVITY"; - - public static final String BROADCAST_STRIDES_UPDATE = "no.nordicsemi.android.nrftoolbox.rsc.BROADCAST_STRIDES_UPDATE"; - public static final String EXTRA_STRIDES = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_STRIDES"; - public static final String EXTRA_DISTANCE = "no.nordicsemi.android.nrftoolbox.rsc.EXTRA_DISTANCE"; - - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - - private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.rsc.ACTION_DISCONNECT"; - - private RSCManager manager; - - /** - * The last value of a cadence - */ - private float cadence; - /** - * Trip distance in cm - */ - private long distance; - /** - * Stride length in cm - */ - private Integer strideLength; - /** - * Number of steps in the trip - */ - private int stepsNumber; - private boolean taskInProgress; - private final Handler handler = new Handler(); - - private final static int NOTIFICATION_ID = 200; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; - - private final LocalBinder binder = new RSCBinder(); - - /** - * This local binder is an interface for the bound activity to operate with the RSC sensor. - */ - class RSCBinder extends LocalBinder { - // empty - } - - @Override - protected LocalBinder getBinder() { - return binder; - } - - @Override - protected LoggableBleManager initializeManager() { - return manager = new RSCManager(this); - } - - @Override - public void onCreate() { - super.onCreate(); - - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(disconnectActionBroadcastReceiver, filter); - } - - @Override - public void onDestroy() { - // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - stopForegroundService(); - unregisterReceiver(disconnectActionBroadcastReceiver); - - super.onDestroy(); - } - - @Override - protected void onRebind() { - stopForegroundService(); - - if (isConnected()) { - // This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property). - // If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications. - manager.readBatteryLevelCharacteristic(); - } - } - - @Override - protected void onUnbind() { - // When we are connected, but the application is not open, we are not really interested in battery level notifications. - // But we will still be receiving other values, if enabled. - if (isConnected()) - manager.disableBatteryLevelCharacteristicNotifications(); - - startForegroundService(); - } - - private final Runnable updateStridesTask = new Runnable() { - @Override - public void run() { - if (!isConnected()) - return; - - stepsNumber++; - distance += strideLength; // [cm] - final Intent broadcast = new Intent(BROADCAST_STRIDES_UPDATE); - broadcast.putExtra(EXTRA_STRIDES, stepsNumber); - broadcast.putExtra(EXTRA_DISTANCE, distance); - LocalBroadcastManager.getInstance(RSCService.this).sendBroadcast(broadcast); - - if (cadence > 0) { - final long interval = (long) (1000.0f * 60.0f / cadence); - handler.postDelayed(updateStridesTask, interval); - } else { - taskInProgress = false; - } - } - }; - - @Override - public void onRSCMeasurementReceived(@NonNull final BluetoothDevice device, final boolean running, - final float instantaneousSpeed, final int instantaneousCadence, - @Nullable final Integer strideLength, - @Nullable final Long totalDistance) { - final Intent broadcast = new Intent(BROADCAST_RSC_MEASUREMENT); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_SPEED, instantaneousSpeed); - broadcast.putExtra(EXTRA_CADENCE, instantaneousCadence); - broadcast.putExtra(EXTRA_STRIDE_LENGTH, strideLength); - broadcast.putExtra(EXTRA_TOTAL_DISTANCE, totalDistance); - broadcast.putExtra(EXTRA_ACTIVITY, running); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - // Start strides counter if not in progress - cadence = instantaneousCadence; - if (strideLength != null) { - this.strideLength = strideLength; - } - if (!taskInProgress && strideLength != null && instantaneousCadence > 0) { - taskInProgress = true; - - final long interval = (long) (1000.0f * 60.0f / cadence); - handler.postDelayed(updateStridesTask, interval); - } - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_BATTERY_LEVEL); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_BATTERY_LEVEL, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Sets the service as a foreground service - */ - private void startForegroundService(){ - // when the activity closes we need to show the notification that user is connected to the peripheral sensor - // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services - final Notification notification = createNotification(R.string.rsc_notification_connected_message, 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - } - - /** - * Stops the service as a foreground service - */ - private void 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) { - stopForeground(true); - } else { - cancelNotification(); - } - } - - /** - * Creates the notification - * - * @param messageResId message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults - */ - @SuppressWarnings("SameParameterValue") - private Notification createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, RSCActivity.class); - - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - - // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); - builder.setContentIntent(pendingIntent); - builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); - builder.setSmallIcon(R.drawable.ic_stat_notify_rsc); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.rsc_notification_action_disconnect), disconnectAction)); - - return builder.build(); - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } - - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver disconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsActivity.java deleted file mode 100644 index fb22c166..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsActivity.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2015, 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.rsc.settings; - -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.MenuItem; - -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsActivity extends AppCompatActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Display the fragment as the main content. - getSupportFragmentManager().beginTransaction().replace(R.id.content, new SettingsFragment()).commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsFragment.java deleted file mode 100644 index 367309be..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/rsc/settings/SettingsFragment.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2015, 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.rsc.settings; - -import android.os.Bundle; -import android.preference.PreferenceFragment; - -import androidx.preference.PreferenceFragmentCompat; -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsFragment extends PreferenceFragmentCompat { - public static final String SETTINGS_UNIT = "settings_rsc_unit"; - public static final int SETTINGS_UNIT_M_S = 0; // [m/s] - public static final int SETTINGS_UNIT_KM_H = 1; // [m/s] - public static final int SETTINGS_UNIT_MPH = 2; // [m/s] - public static final int SETTINGS_UNIT_DEFAULT = SETTINGS_UNIT_KM_H; - - @Override - public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.settings_rsc); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java deleted file mode 100644 index 085bdb1d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/DeviceListAdapter.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright (c) 2015, 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.scanner; - -import android.bluetooth.BluetoothDevice; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.support.v18.scanner.ScanResult; - -/** - * DeviceListAdapter class is list adapter for showing scanned Devices name, address and RSSI image based on RSSI values. - */ -class DeviceListAdapter extends BaseAdapter { - private static final int TYPE_TITLE = 0; - private static final int TYPE_ITEM = 1; - private static final int TYPE_EMPTY = 2; - - private final ArrayList listBondedValues = new ArrayList<>(); - private final ArrayList listValues = new ArrayList<>(); - - DeviceListAdapter() { - } - - /** - * Sets a list of bonded devices. - * @param devices list of bonded devices. - */ - void addBondedDevices(@NonNull final Set devices) { - for (BluetoothDevice device : devices) { - listBondedValues.add(new ExtendedBluetoothDevice(device)); - } - notifyDataSetChanged(); - } - - /** - * Updates the list of not bonded devices. - * @param results list of results from the scanner - */ - public void update(@NonNull final List results) { - for (final ScanResult result : results) { - final ExtendedBluetoothDevice device = findDevice(result); - if (device == null) { - listValues.add(new ExtendedBluetoothDevice(result)); - } else { - device.name = result.getScanRecord() != null ? result.getScanRecord().getDeviceName() : null; - device.rssi = result.getRssi(); - } - } - notifyDataSetChanged(); - } - - private ExtendedBluetoothDevice findDevice(@NonNull final ScanResult result) { - for (final ExtendedBluetoothDevice device : listBondedValues) - if (device.matches(result)) - return device; - for (final ExtendedBluetoothDevice device : listValues) - if (device.matches(result)) - return device; - return null; - } - - void clearDevices() { - listValues.clear(); - notifyDataSetChanged(); - } - - @Override - public int getCount() { - final int bondedCount = listBondedValues.size() + 1; // 1 for the title - final int availableCount = listValues.isEmpty() ? 2 : listValues.size() + 1; // 1 for title, 1 for empty text - if (bondedCount == 1) - return availableCount; - return bondedCount + availableCount; - } - - @Override - public Object getItem(final int position) { - final int bondedCount = listBondedValues.size() + 1; // 1 for the title - if (listBondedValues.isEmpty()) { - if (position == 0) - return R.string.scanner_subtitle_not_bonded; - else - return listValues.get(position - 1); - } else { - if (position == 0) - return R.string.scanner_subtitle_bonded; - if (position < bondedCount) - return listBondedValues.get(position - 1); - if (position == bondedCount) - return R.string.scanner_subtitle_not_bonded; - return listValues.get(position - bondedCount - 1); - } - } - - @Override - public int getViewTypeCount() { - return 3; - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public boolean isEnabled(final int position) { - return getItemViewType(position) == TYPE_ITEM; - } - - @Override - public int getItemViewType(final int position) { - if (position == 0) - return TYPE_TITLE; - - if (!listBondedValues.isEmpty() && position == listBondedValues.size() + 1) - return TYPE_TITLE; - - if (position == getCount() - 1 && listValues.isEmpty()) - return TYPE_EMPTY; - - return TYPE_ITEM; - } - - @Override - public long getItemId(final int position) { - return position; - } - - @Override - public View getView(final int position, @Nullable final View oldView, @NonNull final ViewGroup parent) { - final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - final int type = getItemViewType(position); - - View view = oldView; - switch (type) { - case TYPE_EMPTY: - if (view == null) { - view = inflater.inflate(R.layout.device_list_empty, parent, false); - } - break; - case TYPE_TITLE: - if (view == null) { - view = inflater.inflate(R.layout.device_list_title, parent, false); - } - final TextView title = (TextView) view; - title.setText((Integer) getItem(position)); - break; - default: - if (view == null) { - view = inflater.inflate(R.layout.device_list_row, parent, false); - final ViewHolder holder = new ViewHolder(); - holder.name = view.findViewById(R.id.name); - holder.address = view.findViewById(R.id.address); - holder.rssi = view.findViewById(R.id.rssi); - view.setTag(holder); - } - - final ExtendedBluetoothDevice device = (ExtendedBluetoothDevice) getItem(position); - final ViewHolder holder = (ViewHolder) view.getTag(); - final String name = device.name; - holder.name.setText(name != null ? name : parent.getContext().getString(R.string.not_available)); - holder.address.setText(device.device.getAddress()); - if (!device.isBonded || device.rssi != ExtendedBluetoothDevice.NO_RSSI) { - final int rssiPercent = (int) (100.0f * (127.0f + device.rssi) / (127.0f + 20.0f)); - holder.rssi.setImageLevel(rssiPercent); - holder.rssi.setVisibility(View.VISIBLE); - } else { - holder.rssi.setVisibility(View.GONE); - } - break; - } - - return view; - } - - private class ViewHolder { - private TextView name; - private TextView address; - private ImageView rssi; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java deleted file mode 100644 index 39cf69c0..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ExtendedBluetoothDevice.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2015, 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.scanner; - -import android.bluetooth.BluetoothDevice; - -import androidx.annotation.NonNull; -import no.nordicsemi.android.support.v18.scanner.ScanResult; - -@SuppressWarnings("WeakerAccess") -public class ExtendedBluetoothDevice { - /* package */ static final int NO_RSSI = -1000; - public final BluetoothDevice device; - /** The name is not parsed by some Android devices, f.e. Sony Xperia Z1 with Android 4.3 (C6903). It needs to be parsed manually. */ - public String name; - public int rssi; - public boolean isBonded; - - public ExtendedBluetoothDevice(@NonNull final ScanResult scanResult) { - this.device = scanResult.getDevice(); - this.name = scanResult.getScanRecord() != null ? scanResult.getScanRecord().getDeviceName() : null; - this.rssi = scanResult.getRssi(); - this.isBonded = false; - } - - public ExtendedBluetoothDevice(@NonNull final BluetoothDevice device) { - this.device = device; - this.name = device.getName(); - this.rssi = NO_RSSI; - this.isBonded = true; - } - - public boolean matches(@NonNull final ScanResult scanResult) { - return device.getAddress().equals(scanResult.getDevice().getAddress()); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java deleted file mode 100644 index a4f85905..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/scanner/ScannerFragment.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (c) 2015, 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.scanner; - -import android.Manifest; -import android.app.Dialog; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.os.Handler; -import android.os.ParcelUuid; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.ActivityCompat; -import androidx.fragment.app.DialogFragment; -import androidx.core.content.ContextCompat; -import androidx.appcompat.app.AlertDialog; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.ListView; -import android.widget.Toast; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.support.v18.scanner.BluetoothLeScannerCompat; -import no.nordicsemi.android.support.v18.scanner.ScanCallback; -import no.nordicsemi.android.support.v18.scanner.ScanFilter; -import no.nordicsemi.android.support.v18.scanner.ScanResult; -import no.nordicsemi.android.support.v18.scanner.ScanSettings; - -/** - * ScannerFragment class scan required BLE devices and shows them in a list. This class scans and filter - * devices with standard BLE Service UUID and devices with custom BLE Service UUID. It contains a - * list and a button to scan/cancel. There is a interface {@link OnDeviceSelectedListener} which is - * implemented by activity in order to receive selected device. The scanning will continue to scan - * for 5 seconds and then stop. - */ -public class ScannerFragment extends DialogFragment { - private final static String TAG = "ScannerFragment"; - - private final static String PARAM_UUID = "param_uuid"; - private final static long SCAN_DURATION = 5000; - - private final static int REQUEST_PERMISSION_REQ_CODE = 34; // any 8-bit number - - private BluetoothAdapter bluetoothAdapter; - private OnDeviceSelectedListener listener; - private DeviceListAdapter adapter; - private final Handler handler = new Handler(); - private Button scanButton; - - private View permissionRationale; - - private ParcelUuid uuid; - - private boolean scanning = false; - - public static ScannerFragment getInstance(final UUID uuid) { - final ScannerFragment fragment = new ScannerFragment(); - - final Bundle args = new Bundle(); - if (uuid != null) - args.putParcelable(PARAM_UUID, new ParcelUuid(uuid)); - fragment.setArguments(args); - return fragment; - } - - /** - * Interface required to be implemented by activity. - */ - public interface OnDeviceSelectedListener { - /** - * Fired when user selected the device. - * - * @param device - * the device to connect to - * @param name - * the device name. Unfortunately on some devices {@link BluetoothDevice#getName()} - * always returns null, i.e. Sony Xperia Z1 (C6903) with Android 4.3. - * The name has to be parsed manually form the Advertisement packet. - */ - void onDeviceSelected(@NonNull final BluetoothDevice device, @Nullable final String name); - - /** - * Fired when scanner dialog has been cancelled without selecting a device. - */ - void onDialogCanceled(); - } - - /** - * This will make sure that {@link OnDeviceSelectedListener} interface is implemented by activity. - */ - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - try { - this.listener = (OnDeviceSelectedListener) context; - } catch (final ClassCastException e) { - throw new ClassCastException(context.toString() + " must implement OnDeviceSelectedListener"); - } - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Bundle args = getArguments(); - if (args != null && args.containsKey(PARAM_UUID)) { - uuid = args.getParcelable(PARAM_UUID); - } - - final BluetoothManager manager = (BluetoothManager) requireContext().getSystemService(Context.BLUETOOTH_SERVICE); - if (manager != null) { - bluetoothAdapter = manager.getAdapter(); - } - } - - @Override - public void onDestroyView() { - stopScan(); - super.onDestroyView(); - } - - @NonNull - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); - final View dialogView = LayoutInflater.from(requireContext()) - .inflate(R.layout.fragment_device_selection, null); - final ListView listview = dialogView.findViewById(android.R.id.list); - - listview.setEmptyView(dialogView.findViewById(android.R.id.empty)); - listview.setAdapter(adapter = new DeviceListAdapter()); - - builder.setTitle(R.string.scanner_title); - final AlertDialog dialog = builder.setView(dialogView).create(); - listview.setOnItemClickListener((parent, view, position, id) -> { - stopScan(); - dialog.dismiss(); - final ExtendedBluetoothDevice d = (ExtendedBluetoothDevice) adapter.getItem(position); - listener.onDeviceSelected(d.device, d.name); - }); - - permissionRationale = dialogView.findViewById(R.id.permission_rationale); // this is not null only on API23+ - - scanButton = dialogView.findViewById(R.id.action_cancel); - scanButton.setOnClickListener(v -> { - if (v.getId() == R.id.action_cancel) { - if (scanning) { - dialog.cancel(); - } else { - startScan(); - } - } - }); - - addBoundDevices(); - if (savedInstanceState == null) - startScan(); - return dialog; - } - - @Override - public void onCancel(@NonNull DialogInterface dialog) { - super.onCancel(dialog); - - listener.onDialogCanceled(); - } - - @Override - public void onRequestPermissionsResult(final int requestCode, final @NonNull String[] permissions, final @NonNull int[] grantResults) { - switch (requestCode) { - case REQUEST_PERMISSION_REQ_CODE: { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // We have been granted the Manifest.permission.ACCESS_FINE_LOCATION permission. Now we may proceed with scanning. - startScan(); - } else { - permissionRationale.setVisibility(View.VISIBLE); - Toast.makeText(getActivity(), R.string.no_required_permission, Toast.LENGTH_SHORT).show(); - } - break; - } - } - } - - /** - * Scan for 5 seconds and then stop scanning when a BluetoothLE device is found then lEScanCallback - * is activated This will perform regular scan for custom BLE Service UUID and then filter out. - * using class ScannerServiceParser - */ - private void startScan() { - // Since Android 6.0 we need to obtain Manifest.permission.ACCESS_FINE_LOCATION to be able to scan for - // Bluetooth LE devices. This is related to beacons as proximity devices. - // On API older than Marshmallow the following code does nothing. - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - // When user pressed Deny and still wants to use this functionality, show the rationale - if (ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), Manifest.permission.ACCESS_FINE_LOCATION) && permissionRationale.getVisibility() == View.GONE) { - permissionRationale.setVisibility(View.VISIBLE); - return; - } - - requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_PERMISSION_REQ_CODE); - return; - } - - // Hide the rationale message, we don't need it anymore. - if (permissionRationale != null) - permissionRationale.setVisibility(View.GONE); - - adapter.clearDevices(); - scanButton.setText(R.string.scanner_action_cancel); - - final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); - final ScanSettings settings = new ScanSettings.Builder() - .setLegacy(false) - .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).setReportDelay(1000).setUseHardwareBatchingIfSupported(false).build(); - final List filters = new ArrayList<>(); - filters.add(new ScanFilter.Builder().setServiceUuid(uuid).build()); - scanner.startScan(filters, settings, scanCallback); - - scanning = true; - handler.postDelayed(() -> { - if (scanning) { - stopScan(); - } - }, SCAN_DURATION); - } - - /** - * Stop scan if user tap Cancel button - */ - private void stopScan() { - if (scanning) { - scanButton.setText(R.string.scanner_action_scan); - - final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); - scanner.stopScan(scanCallback); - - scanning = false; - } - } - - private ScanCallback scanCallback = new ScanCallback() { - @Override - public void onScanResult(final int callbackType, @NonNull final ScanResult result) { - // do nothing - } - - @Override - public void onBatchScanResults(@NonNull final List results) { - adapter.update(results); - } - - @Override - public void onScanFailed(final int errorCode) { - // should never be called - } - }; - - private void addBoundDevices() { - final Set devices = bluetoothAdapter.getBondedDevices(); - adapter.addBondedDevices(devices); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt deleted file mode 100644 index aa3ffa43..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/Readme.txt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2015, 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. - */ - - nRF Toolbox demonstrates how to implement the Bluetooth Smart features in an Android application. - It consists of number of profiles, like Heart Rate, Blood Pressure etc. that may be use as is to communicate with real devices. - They use the Bluetooth SIG adopted profiles, that may be found here: https://developer.bluetooth.org/gatt/profiles/Pages/ProfilesHome.aspx - - The Template Profile has been created to give a quick start with implementing proprietary services. Just start modifying 4 classes inside - the template package to implement features you need. - - Below you will find a short step-by-step tutorial: - - 1. The template consists of the following files: - - TemplateActivity - the main class that is responsible for managing the view of your profile - - TemplateService - the service that is started whenever you connect to a device. I handles the Bluetooth Smart communication using the... - - TemplateManager - the manager that handles all the BLE logic required to communicate with the device. The TemplateManager derives from - the BleManager which handles most of the event itself and propagates the data-relevant to deriving class. You don't - have to, or even shouldn't modify the BleManager (unless you want to change the default behaviour). - - TemplateManagerCallbacks - the interface with a list of callbacks that the TemplateManager can call. Each method is usually related to one - BLE event, e.g. receiving a new value of the characteristic.\ - - TemplateParser - an optional class in the .parser package that is responsible for converting the characteristic value to String. - This is used only for debugging. The String returned by the parse(..) method is then logged into the nRF Logger application - (if such installed). - - /settings/SettingsActivity and /settings/SettingsFragment - classes used to present user preferences. A stub implementation in the template. - - /res/layout/activity_feature_template.xml - the layout file for the TemplateActivity - - /res/values/strings_template.xml - a set of strings used in the layout file - - /res/xml/settings/template.xml - the user settings configuration - - /res/drawable/(x)hdpi/ic_template_feature.png - the template profile icon (HDPI, XHDPI). Please, keep the files size. - - /res/drawable/(x)hdpi/ic_stat_notify_template - the icon that is used in the notification - -2. The communication between the components goes as follows: - - User clicks the CONNECT button and selects a target device on TemplateActivity. - - The base class of the TemplateActivity starts the service given by getServiceClass() method. - - The service starts and initializes the TemplateManager. TemplateActivity binds to the service and is being given the TemplateBinder object (the service API) as a result. - - The manager connects to the device using Bluetooth Smart and discovers its services. - - The manager initializes the device. Initialization is done using the list of actions given by the initGatt(..) method in the TemplateManager. - Initialization usually contains enabling notifications, writing some initial values etc. - - When initialization is complete the manager calls the onDeviceReady() callback. - - The service sends the BROADCAST_DEVICE_READY broadcast to the activity. Communication from the Service to the Activity is always done using the LocalBroadcastManager broadcasts. - - The base class of the TemplateActivity listens to the broadcasts and calls appropriate methods. - - - When a custom event occurs, for example a notification is received, the manager parses the incoming data and calls the proper callback. - - The callback implementation in the TemplateService sends a broadcast message with values given in parameters. - - The TemplateActivity, which had registered a broadcast receiver before, listens to the broadcasts, reads the values and present them to users. - - - Communication Activity->Service is done using the API in the TemplateBinder. You may find the example of how to use it in the ProximityActivity. - -3. Please read the files listed above and the TODO messages for more information what to modify in the files. - -4. Remember to add your activities and the service in the AndroidManifest.xml file. The nRF Toolbox lists all activities with the following intent filter: - - - - - -5. Feel free to rename the nRF Toolbox application (/res/values/strings.xml ->app_name), change the toolbar colors (/res/values/color.xml -> actionBarColor, actionBarColorDark). - In order to remove unused profiles from the main FeaturesActivity just comment out their intent-filter tags in the AndroidManifest.xml file. diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java deleted file mode 100644 index 48bc276c..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateActivity.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (c) 2015, 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.template; - -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.view.Menu; -import android.widget.TextView; - -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; -import no.nordicsemi.android.nrftoolbox.template.settings.SettingsActivity; - -/** - * Modify the Template Activity to match your needs. - */ -public class TemplateActivity extends BleProfileServiceReadyActivity { - @SuppressWarnings("unused") - private final String TAG = "TemplateActivity"; - - // TODO change view references to match your need - private TextView valueView; - private TextView batteryLevelView; - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - // TODO modify the layout file(s). By default the activity shows only one field - the Heart Rate value as a sample - setContentView(R.layout.activity_feature_template); - setGUI(); - } - - private void setGUI() { - // TODO assign your views to fields - valueView = findViewById(R.id.value); - batteryLevelView = findViewById(R.id.battery); - - findViewById(R.id.action_set_name).setOnClickListener(v -> { - if (isDeviceConnected()) { - getService().performAction("Template"); - } - }); - } - - @Override - protected void onInitialize(final Bundle savedInstanceState) { - LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, makeIntentFilter()); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver); - } - - @Override - protected void setDefaultUI() { - // TODO clear your UI - valueView.setText(R.string.not_available_value); - batteryLevelView.setText(R.string.not_available); - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.template_feature_title; - } - - @Override - protected int getAboutTextId() { - return R.string.template_about_text; - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.settings_and_about, menu); - return true; - } - - @Override - protected boolean onOptionsItemSelected(final int itemId) { - switch (itemId) { - case R.id.action_settings: - final Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - break; - } - return true; - } - - @Override - protected int getDefaultDeviceName() { - return R.string.template_default_name; - } - - @Override - protected UUID getFilterUUID() { - // TODO this method may return the UUID of the service that is required to be in the advertisement packet of a device in order to be listed on the Scanner dialog. - // If null is returned no filtering is done. - return TemplateManager.SERVICE_UUID; - } - - @Override - protected Class getServiceClass() { - return TemplateService.class; - } - - @Override - protected void onServiceBound(final TemplateService.TemplateBinder binder) { - // not used - } - - @Override - protected void onServiceUnbound() { - // not used - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // this may notify user or show some views - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - batteryLevelView.setText(R.string.not_available); - } - - // Handling updates from the device - @SuppressWarnings("unused") - private void setValueOnView(@NonNull final BluetoothDevice device, final int value) { - // TODO assign the value to a view - valueView.setText(String.valueOf(value)); - } - - @SuppressWarnings("unused") - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int value) { - batteryLevelView.setText(getString(R.string.battery, value)); - } - - private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final String action = intent.getAction(); - final BluetoothDevice device = intent.getParcelableExtra(TemplateService.EXTRA_DEVICE); - - if (TemplateService.BROADCAST_TEMPLATE_MEASUREMENT.equals(action)) { - final int value = intent.getIntExtra(TemplateService.EXTRA_DATA, 0); - // Update GUI - setValueOnView(device, value); - } else if (TemplateService.BROADCAST_BATTERY_LEVEL.equals(action)) { - final int batteryLevel = intent.getIntExtra(TemplateService.EXTRA_BATTERY_LEVEL, 0); - // Update GUI - onBatteryLevelChanged(device, batteryLevel); - } - } - }; - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(TemplateService.BROADCAST_TEMPLATE_MEASUREMENT); - intentFilter.addAction(TemplateService.BROADCAST_BATTERY_LEVEL); - return intentFilter; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java deleted file mode 100644 index 60b262cb..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManager.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (c) 2015, 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.template; - -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import androidx.annotation.NonNull; -import android.util.Log; - -import java.util.UUID; - -import no.nordicsemi.android.ble.BleManager; -import no.nordicsemi.android.ble.data.Data; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.battery.BatteryManager; -import no.nordicsemi.android.nrftoolbox.parser.TemplateParser; -import no.nordicsemi.android.nrftoolbox.template.callback.TemplateDataCallback; - -/** - * Modify to template manager to match your requirements. - * The TemplateManager extends {@link BatteryManager}, but it may easily extend {@link BleManager} - * instead if you don't need Battery Service support. If not, also modify the - * {@link TemplateManagerCallbacks} to extend {@link no.nordicsemi.android.ble.BleManagerCallbacks} - * and replace BatteryManagerGattCallback to BleManagerGattCallback in this class. - */ -public class TemplateManager extends BatteryManager { - // TODO Replace the services and characteristics below to match your device. - /** - * The service UUID. - */ - static final UUID SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb"); // Heart Rate service - /** - * A UUID of a characteristic with notify property. - */ - private static final UUID MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb"); // Heart Rate Measurement - /** - * A UUID of a characteristic with read property. - */ - private static final UUID READABLE_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb"); // Body Sensor Location - /** - * Some other service UUID. - */ - private static final UUID OTHER_SERVICE_UUID = UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"); // Generic Access service - /** - * A UUID of a characteristic with write property. - */ - private static final UUID WRITABLE_CHARACTERISTIC_UUID = UUID.fromString("00002A00-0000-1000-8000-00805f9b34fb"); // Device Name - - // TODO Add more services and characteristics references. - private BluetoothGattCharacteristic requiredCharacteristic, deviceNameCharacteristic, optionalCharacteristic; - - public TemplateManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BatteryManagerGattCallback getGattCallback() { - return new TemplateManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ - private class TemplateManagerGattCallback extends BatteryManagerGattCallback { - - @Override - protected void initialize() { - // Initialize the Battery Manager. It will enable Battery Level notifications. - // Remove it if you don't need this feature. - super.initialize(); - - // TODO Initialize your manager here. - // Initialization is done once, after the device is connected. Usually it should - // enable notifications or indications on some characteristics, write some data or - // read some features / version. - // After the initialization is complete, the onDeviceReady(...) method will be called. - - // Increase the MTU - requestMtu(43) - .with((device, mtu) -> log(LogContract.Log.Level.APPLICATION, "MTU changed to " + mtu)) - .done(device -> { - // You may do some logic in here that should be done when the request finished successfully. - // In case of MTU this method is called also when the MTU hasn't changed, or has changed - // to a different (lower) value. Use .with(...) to get the MTU value. - }) - .fail((device, status) -> log(Log.WARN, "MTU change not supported")) - .enqueue(); - - // Set notification callback - setNotificationCallback(requiredCharacteristic) - // This callback will be called each time the notification is received - .with(new TemplateDataCallback() { - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(LogContract.Log.Level.APPLICATION, TemplateParser.parse(data)); - super.onDataReceived(device, data); - } - - @Override - public void onSampleValueReceived(@NonNull final BluetoothDevice device, final int value) { - // Let's lass received data to the service - mCallbacks.onSampleValueReceived(device, value); - } - - @Override - public void onInvalidDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - log(Log.WARN, "Invalid data received: " + data); - } - }); - - // Enable notifications - enableNotifications(requiredCharacteristic) - // Method called after the data were sent (data will contain 0x0100 in this case) - .with((device, data) -> log(Log.DEBUG, "Data sent: " + data)) - // Method called when the request finished successfully. This will be called after .with(..) callback - .done(device -> log(LogContract.Log.Level.APPLICATION, "Notifications enabled successfully")) - // Methods called in case of an error, for example when the characteristic does not have Notify property - .fail((device, status) -> log(Log.WARN, "Failed to enable notifications")) - .enqueue(); - } - - @Override - protected boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - // TODO Initialize required characteristics. - // It should return true if all has been discovered (that is that device is supported). - final BluetoothGattService service = gatt.getService(SERVICE_UUID); - if (service != null) { - requiredCharacteristic = service.getCharacteristic(MEASUREMENT_CHARACTERISTIC_UUID); - } - final BluetoothGattService otherService = gatt.getService(OTHER_SERVICE_UUID); - if (otherService != null) { - deviceNameCharacteristic = otherService.getCharacteristic(WRITABLE_CHARACTERISTIC_UUID); - } - return requiredCharacteristic != null && deviceNameCharacteristic != null; - } - - @Override - protected boolean isOptionalServiceSupported(@NonNull final BluetoothGatt gatt) { - // Initialize Battery characteristic - super.isOptionalServiceSupported(gatt); - - // TODO If there are some optional characteristics, initialize them there. - final BluetoothGattService service = gatt.getService(SERVICE_UUID); - if (service != null) { - optionalCharacteristic = service.getCharacteristic(READABLE_CHARACTERISTIC_UUID); - } - return optionalCharacteristic != null; - } - - @Override - protected void onDeviceDisconnected() { - // Release Battery Service - super.onDeviceDisconnected(); - - // TODO Release references to your characteristics. - requiredCharacteristic = null; - deviceNameCharacteristic = null; - optionalCharacteristic = null; - } - - @Override - protected void onDeviceReady() { - super.onDeviceReady(); - - // Initialization is now ready. - // The service or activity has been notified with TemplateManagerCallbacks#onDeviceReady(). - // TODO Do some extra logic here, of remove onDeviceReady(). - - // Device is ready, let's read something here. Usually there is nothing else to be done - // here, as all had been done during initialization. - readCharacteristic(optionalCharacteristic) - .with((device, data) -> { - // Characteristic value has been read - // Let's do some magic with it. - if (data.size() > 0) { - final Integer value = data.getIntValue(Data.FORMAT_UINT8, 0); - log(LogContract.Log.Level.APPLICATION, "Value '" + value + "' has been read!"); - } else { - log(Log.WARN, "Value is empty!"); - } - }) - .enqueue(); - } - } - - // TODO Define manager's API - - /** - * This method will write important data to the device. - * - * @param parameter parameter to be written. - */ - void performAction(final String parameter) { - log(Log.VERBOSE, "Changing device name to \"" + parameter + "\""); - // Write some data to the characteristic. - writeCharacteristic(deviceNameCharacteristic, Data.from(parameter)) - // If data are longer than MTU-3, they will be chunked into multiple packets. - // Check out other split options, with .split(...). - .split() - // Callback called when data were sent, or added to outgoing queue in case - // Write Without Request type was used. - .with((device, data) -> log(Log.DEBUG, data.size() + " bytes were sent")) - // Callback called when data were sent, or added to outgoing queue in case - // Write Without Request type was used. This is called after .with(...) callback. - .done(device -> log(LogContract.Log.Level.APPLICATION, "Device name set to \"" + parameter + "\"")) - // Callback called when write has failed. - .fail((device, status) -> log(Log.WARN, "Failed to change device name")) - .enqueue(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java deleted file mode 100644 index 05b03d88..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateManagerCallbacks.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2015, 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.template; - -import no.nordicsemi.android.nrftoolbox.battery.BatteryManagerCallbacks; -import no.nordicsemi.android.nrftoolbox.template.callback.TemplateCharacteristicCallback; - -/** - * Interface {@link TemplateManagerCallbacks} must be implemented by {@link TemplateService} - * in order to receive callbacks from {@link TemplateManager} - */ -interface TemplateManagerCallbacks extends BatteryManagerCallbacks, TemplateCharacteristicCallback { - - // Callbacks are called when a data has been received/written to a remote device. - // This is the way how the manager notifies the activity about this event. - - // TODO add more callbacks. - // If you need, create more ...Callback interfaces and extend this interface with them. -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java deleted file mode 100644 index e869c2ea..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/TemplateService.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (c) 2015, 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.template; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -public class TemplateService extends BleProfileService implements TemplateManagerCallbacks { - public static final String BROADCAST_TEMPLATE_MEASUREMENT = "no.nordicsemi.android.nrftoolbox.template.BROADCAST_MEASUREMENT"; - public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.template.EXTRA_DATA"; - - public static final String BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"; - public static final String EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"; - - private final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.template.ACTION_DISCONNECT"; - - private final static int NOTIFICATION_ID = 864; - private final static int OPEN_ACTIVITY_REQ = 0; - private final static int DISCONNECT_REQ = 1; - - private TemplateManager manager; - - private final LocalBinder binder = new TemplateBinder(); - - /** - * This local binder is an interface for the bound activity to operate with the sensor. - */ - class TemplateBinder extends LocalBinder { - // TODO Define service API that may be used by a bound Activity - - /** - * Sends some important data to the device. - * - * @param parameter some parameter. - */ - void performAction(final String parameter) { - manager.performAction(parameter); - } - } - - @Override - protected LocalBinder getBinder() { - return binder; - } - - @Override - protected LoggableBleManager initializeManager() { - return manager = new TemplateManager(this); - } - - @Override - public void onCreate() { - super.onCreate(); - - final IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DISCONNECT); - registerReceiver(disconnectActionBroadcastReceiver, filter); - } - - @Override - public void onDestroy() { - // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - stopForegroundService(); - unregisterReceiver(disconnectActionBroadcastReceiver); - - super.onDestroy(); - } - - @Override - protected void onRebind() { - stopForegroundService(); - } - - @Override - protected void onUnbind() { - startForegroundService(); - } - - @Override - public void onSampleValueReceived(@NonNull final BluetoothDevice device, final int value) { - final Intent broadcast = new Intent(BROADCAST_TEMPLATE_MEASUREMENT); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, value); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - if (!bound) { - // Here we may update the notification to display the current value. - // TODO modify the notification here - } - } - - @Override - public void onBatteryLevelChanged(@NonNull final BluetoothDevice device, final int batteryLevel) { - - } - - /** - * Sets the service as a foreground service - */ - private void startForegroundService(){ - // when the activity closes we need to show the notification that user is connected to the peripheral sensor - // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services - final Notification notification = createNotification(R.string.template_notification_connected_message, 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - } - - /** - * Stops the service as a foreground service - */ - private void 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) { - stopForeground(true); - } else { - cancelNotification(); - } - } - - /** - * Creates the notification. - * - * @param messageResId message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults signals that will be used to notify the user - */ - @SuppressWarnings("SameParameterValue") - private Notification createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, TemplateActivity.class); - - final Intent disconnect = new Intent(ACTION_DISCONNECT); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - - // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); - builder.setContentIntent(pendingIntent); - builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); - builder.setSmallIcon(R.drawable.ic_stat_notify_template); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.template_notification_action_disconnect), disconnectAction)); - - return builder.build(); - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } - - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver disconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java deleted file mode 100644 index 34e6d445..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateCharacteristicCallback.java +++ /dev/null @@ -1,21 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.template.callback; - -import android.bluetooth.BluetoothDevice; -import androidx.annotation.NonNull; - -/** - * This class defines your characteristic API. - * In this example (that is the HRM characteristic, which the template is based on), is notifying - * with a value (Heart Rate). The single method just returns the value and ignores other - * optional data from Heart Rate Measurement characteristic for simplicity. - */ -public interface TemplateCharacteristicCallback { - - /** - * Called when a value is received. - * - * @param device a device from which the value was obtained. - * @param value the new value. - */ - void onSampleValueReceived(@NonNull final BluetoothDevice device, int value); -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java deleted file mode 100644 index 1ec05f94..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/callback/TemplateDataCallback.java +++ /dev/null @@ -1,47 +0,0 @@ -package no.nordicsemi.android.nrftoolbox.template.callback; - -import android.bluetooth.BluetoothDevice; -import androidx.annotation.NonNull; - -import no.nordicsemi.android.ble.callback.profile.ProfileDataCallback; -import no.nordicsemi.android.ble.data.Data; - -/** - * This is a sample data callback, that's based on Heart Rate Measurement characteristic. - * It parses the HR value and ignores other optional data for simplicity. - * Check {@link no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback} - * for full implementation. - * - * TODO Modify the content to parse your data. - */ -@SuppressWarnings("ConstantConditions") -public abstract class TemplateDataCallback implements ProfileDataCallback, TemplateCharacteristicCallback { - - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, @NonNull final Data data) { - if (data.size() < 2) { - onInvalidDataReceived(device, data); - return; - } - - // Read flags - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset); - final int hearRateType = (flags & 0x01) == 0 ? Data.FORMAT_UINT8 : Data.FORMAT_UINT16; - offset += 1; - - // Validate packet length. The type's lower nibble is its length. - if (data.size() < 1 + (hearRateType & 0x0F)) { - onInvalidDataReceived(device, data); - return; - } - - final int value = data.getIntValue(hearRateType, offset); - // offset += hearRateType & 0xF; - - // ... - - // Report the parsed value(s) - onSampleValueReceived(device, value); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java deleted file mode 100644 index 00f410dc..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsActivity.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2015, 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.template.settings; - -import android.os.Bundle; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.view.MenuItem; - -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsActivity extends AppCompatActivity { - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_settings); - - final Toolbar toolbar = findViewById(R.id.toolbar_actionbar); - setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - // Display the fragment as the main content. - getSupportFragmentManager().beginTransaction().replace(R.id.content, new SettingsFragment()).commit(); - } - - @Override - public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java deleted file mode 100644 index 3475b49b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/template/settings/SettingsFragment.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2015, 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.template.settings; - -import android.os.Bundle; -import android.preference.PreferenceFragment; - -import androidx.preference.PreferenceFragmentCompat; -import no.nordicsemi.android.nrftoolbox.R; - -public class SettingsFragment extends PreferenceFragmentCompat { - public static final String SETTINGS_DATA = "settings_template_data"; // TODO values matching those in settings_template.xml file in /res/xml - public static final int SETTINGS_VARIANT_A = 0; - public static final int SETTINGS_VARIANT_B = 1; - public static final int SETTINGS_VARIANT_DEFAULT = SETTINGS_VARIANT_A; - - @Override - public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - addPreferencesFromResource(R.xml.settings_template); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java deleted file mode 100644 index d32b8ed5..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTActivity.java +++ /dev/null @@ -1,855 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.Manifest; -import android.animation.ArgbEvaluator; -import android.animation.ValueAnimator; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.TransitionDrawable; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Environment; -import android.os.Handler; -import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import com.google.android.material.snackbar.Snackbar; -import androidx.core.app.ActivityCompat; -import androidx.fragment.app.DialogFragment; -import androidx.core.app.NotificationCompat; -import androidx.core.content.ContextCompat; -import androidx.slidingpanelayout.widget.SlidingPaneLayout; -import androidx.appcompat.app.AlertDialog; -import android.util.Log; -import android.view.Menu; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ListView; -import android.widget.Toast; - -import com.google.android.gms.common.api.GoogleApiClient; - -import org.simpleframework.xml.Serializer; -import org.simpleframework.xml.core.Persister; -import org.simpleframework.xml.strategy.Strategy; -import org.simpleframework.xml.strategy.Type; -import org.simpleframework.xml.strategy.Visitor; -import org.simpleframework.xml.strategy.VisitorStrategy; -import org.simpleframework.xml.stream.Format; -import org.simpleframework.xml.stream.HyphenStyle; -import org.simpleframework.xml.stream.InputNode; -import org.simpleframework.xml.stream.NodeMap; -import org.simpleframework.xml.stream.OutputNode; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.StringWriter; -import java.util.UUID; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.dfu.adapter.FileBrowserAppsAdapter; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileServiceReadyActivity; -import no.nordicsemi.android.nrftoolbox.uart.database.DatabaseHelper; -import no.nordicsemi.android.nrftoolbox.uart.domain.Command; -import no.nordicsemi.android.nrftoolbox.uart.domain.UartConfiguration; -import no.nordicsemi.android.nrftoolbox.uart.wearable.UARTConfigurationSynchronizer; -import no.nordicsemi.android.nrftoolbox.utility.FileHelper; -import no.nordicsemi.android.nrftoolbox.widget.ClosableSpinner; - -public class UARTActivity extends BleProfileServiceReadyActivity implements UARTInterface, - UARTNewConfigurationDialogFragment.NewConfigurationDialogListener, UARTConfigurationsAdapter.ActionListener, AdapterView.OnItemSelectedListener, - GoogleApiClient.ConnectionCallbacks { - private final static String TAG = "UARTActivity"; - - private final static String PREFS_BUTTON_ENABLED = "prefs_uart_enabled_"; - private final static String PREFS_BUTTON_COMMAND = "prefs_uart_command_"; - private final static String PREFS_BUTTON_ICON = "prefs_uart_icon_"; - /** This preference keeps the ID of the selected configuration. */ - private final static String PREFS_CONFIGURATION = "configuration_id"; - /** This preference is set to true when initial data synchronization for wearables has been completed. */ - private final static String PREFS_WEAR_SYNCED = "prefs_uart_synced"; - private final static String SIS_EDIT_MODE = "sis_edit_mode"; - - private final static int SELECT_FILE_REQ = 2678; // random - private final static int REQUEST_SAVE = 2679; - - UARTConfigurationSynchronizer wearableSynchronizer; - - /** The current configuration. */ - private UartConfiguration configuration; - private DatabaseHelper databaseHelper; - private SharedPreferences preferences; - private UARTConfigurationsAdapter configurationsAdapter; - private ClosableSpinner configurationSpinner; - private SlidingPaneLayout slider; - private View container; - private UARTService.UARTBinder serviceBinder; - private ConfigurationListener configurationListener; - private boolean editMode; - - public interface ConfigurationListener { - void onConfigurationModified(); - void onConfigurationChanged(@NonNull final UartConfiguration configuration); - void setEditMode(final boolean editMode); - } - - public void setConfigurationListener(final ConfigurationListener listener) { - configurationListener = listener; - } - - @Override - protected Class getServiceClass() { - return UARTService.class; - } - - @Override - protected int getLoggerProfileTitle() { - return R.string.uart_feature_title; - } - - @Override - protected Uri getLocalAuthorityLogger() { - return UARTLocalLogContentProvider.AUTHORITY_URI; - } - - @Override - protected void setDefaultUI() { - // empty - } - - @Override - protected void onServiceBound(final UARTService.UARTBinder binder) { - serviceBinder = binder; - } - - @Override - protected void onServiceUnbound() { - serviceBinder = null; - } - - @Override - protected void onInitialize(final Bundle savedInstanceState) { - preferences = PreferenceManager.getDefaultSharedPreferences(this); - databaseHelper = new DatabaseHelper(this); - ensureFirstConfiguration(databaseHelper); - configurationsAdapter = new UARTConfigurationsAdapter(this, this, databaseHelper.getConfigurationsNames()); - - // Initialize Wearable synchronizer - wearableSynchronizer = UARTConfigurationSynchronizer.from(this, this); - } - - /** - * Method called when Google API Client connects to Wearable.API. - */ - @Override - public void onConnected(final Bundle bundle) { - // Ensure the Wearable API was connected - if (!wearableSynchronizer.hasConnectedApi()) - return; - - if (!preferences.getBoolean(PREFS_WEAR_SYNCED, false)) { - new Thread(() -> { - final Cursor cursor = databaseHelper.getConfigurations(); - try { - while (cursor.moveToNext()) { - final long id = cursor.getLong(0 /* _ID */); - try { - final String xml = cursor.getString(2 /* XML */); - final Format format = new Format(new HyphenStyle()); - final Serializer serializer = new Persister(format); - final UartConfiguration configuration = serializer.read(UartConfiguration.class, xml); - wearableSynchronizer.onConfigurationAddedOrEdited(id, configuration).await(); - } catch (final Exception e) { - Log.w(TAG, "Deserializing configuration with id " + id + " failed", e); - } - } - preferences.edit().putBoolean(PREFS_WEAR_SYNCED, true).apply(); - } finally { - cursor.close(); - } - }).start(); - } - } - - /** - * Method called then Google API client connection was suspended. - * @param cause the cause of suspension - */ - @Override - public void onConnectionSuspended(final int cause) { - // dp nothing - } - - @Override - protected void onDestroy() { - super.onDestroy(); - wearableSynchronizer.close(); - } - - @Override - protected void onCreateView(final Bundle savedInstanceState) { - setContentView(R.layout.activity_feature_uart); - - container = findViewById(R.id.container); - // Setup the sliding pane if it exists - final SlidingPaneLayout slidingPane = slider = findViewById(R.id.sliding_pane); - if (slidingPane != null) { - slidingPane.setSliderFadeColor(Color.TRANSPARENT); - slidingPane.setShadowResourceLeft(R.drawable.shadow_r); - slidingPane.setPanelSlideListener(new SlidingPaneLayout.SimplePanelSlideListener() { - @Override - public void onPanelClosed(final View panel) { - // Close the keyboard - final UARTLogFragment logFragment = (UARTLogFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_log); - logFragment.onFragmentHidden(); - } - }); - } - } - - @Override - protected void onViewCreated(final Bundle savedInstanceState) { - getSupportActionBar().setDisplayShowTitleEnabled(false); - - final ClosableSpinner configurationSpinner = this.configurationSpinner = findViewById(R.id.toolbar_spinner); - configurationSpinner.setOnItemSelectedListener(this); - configurationSpinner.setAdapter(configurationsAdapter); - configurationSpinner.setSelection(configurationsAdapter.getItemPosition(preferences.getLong(PREFS_CONFIGURATION, 0))); - } - - @Override - protected void onRestoreInstanceState(final @NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - editMode = savedInstanceState.getBoolean(SIS_EDIT_MODE); - setEditMode(editMode, false); - } - - @Override - public void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - - outState.putBoolean(SIS_EDIT_MODE, editMode); - } - - @Override - public void onServicesDiscovered(@NonNull final BluetoothDevice device, final boolean optionalServicesFound) { - // do nothing - } - - @Override - public void onDeviceSelected(@NonNull final BluetoothDevice device, final String name) { - // The super method starts the service - super.onDeviceSelected(device, name); - - // Notify the log fragment about it - final UARTLogFragment logFragment = (UARTLogFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_log); - logFragment.onServiceStarted(); - } - - @Override - protected int getDefaultDeviceName() { - return R.string.uart_default_name; - } - - @Override - protected int getAboutTextId() { - return R.string.uart_about_text; - } - - @Override - protected UUID getFilterUUID() { - return null; // not used - } - - @Override - public void send(final String text) { - if (serviceBinder != null) - serviceBinder.send(text); - } - - public void setEditMode(final boolean editMode) { - setEditMode(editMode, true); - invalidateOptionsMenu(); - } - - @Override - public void onBackPressed() { - if (slider != null && slider.isOpen()) { - slider.closePane(); - return; - } - if (editMode) { - setEditMode(false); - return; - } - super.onBackPressed(); - } - - @Override - public boolean onCreateOptionsMenu(final Menu menu) { - getMenuInflater().inflate(R.menu.uart_menu_configurations, menu); - getMenuInflater().inflate(editMode ? R.menu.uart_menu_config : R.menu.uart_menu, menu); - - final int configurationsCount = databaseHelper.getConfigurationsCount(); - menu.findItem(R.id.action_remove).setVisible(configurationsCount > 1); - return super.onCreateOptionsMenu(menu); - } - - @Override - protected boolean onOptionsItemSelected(int itemId) { - final String name = configuration.getName(); - switch (itemId) { - case R.id.action_configure: - setEditMode(!editMode); - return true; - case R.id.action_show_log: - slider.openPane(); - return true; - case R.id.action_share: { - final String xml = databaseHelper.getConfiguration(configurationSpinner.getSelectedItemId()); - - final Intent intent = new Intent(Intent.ACTION_SEND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setType("text/xml"); - intent.putExtra(Intent.EXTRA_TEXT, xml); - intent.putExtra(Intent.EXTRA_SUBJECT, configuration.getName()); - try { - startActivity(intent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(this, R.string.no_uri_application, Toast.LENGTH_SHORT).show(); - } - return true; - } - case R.id.action_export: { - exportConfiguration(); - return true; - } - case R.id.action_rename: { - final DialogFragment fragment = UARTNewConfigurationDialogFragment.getInstance(name, false); - fragment.show(getSupportFragmentManager(), null); - // onNewConfiguration(name, false) will be called when user press OK - return true; - } - case R.id.action_duplicate: { - final DialogFragment fragment = UARTNewConfigurationDialogFragment.getInstance(name, true); - fragment.show(getSupportFragmentManager(), null); - // onNewConfiguration(name, true) will be called when user press OK - return true; - } - case R.id.action_remove: { - databaseHelper.removeDeletedServerConfigurations(); // just to be sure nothing has left - final UartConfiguration removedConfiguration = configuration; - final long id = databaseHelper.deleteConfiguration(name); - if (id >= 0) - wearableSynchronizer.onConfigurationDeleted(id); - refreshConfigurations(); - - final Snackbar snackbar = Snackbar.make(container, R.string.uart_configuration_deleted, Snackbar.LENGTH_INDEFINITE).setAction(R.string.uart_action_undo, v -> { - final long id1 = databaseHelper.restoreDeletedServerConfiguration(name); - if (id1 >= 0) - wearableSynchronizer.onConfigurationAddedOrEdited(id1, removedConfiguration); - refreshConfigurations(); - }); - snackbar.setDuration(5000); // This is not an error - snackbar.show(); - return true; - } - } - return false; - } - - @Override - public void onItemSelected(final AdapterView parent, final View view, final int position, final long id) { - if (position > 0) { // FIXME this is called twice after rotation. - try { - final String xml = databaseHelper.getConfiguration(id); - final Format format = new Format(new HyphenStyle()); - final Serializer serializer = new Persister(format); - configuration = serializer.read(UartConfiguration.class, xml); - configurationListener.onConfigurationChanged(configuration); - } catch (final Exception e) { - Log.e(TAG, "Selecting configuration failed", e); - - String message; - if (e.getLocalizedMessage() != null) - message = e.getLocalizedMessage(); - else if (e.getCause() != null && e.getCause().getLocalizedMessage() != null) - message = e.getCause().getLocalizedMessage(); - else - message = "Unknown error"; - final String msg = message; - Snackbar.make(container, R.string.uart_configuration_loading_failed, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.uart_action_details, v -> - new AlertDialog.Builder(UARTActivity.this) - .setMessage(msg) - .setTitle(R.string.uart_action_details) - .setPositiveButton(R.string.ok, null) - .show()) - .show(); - return; - } - - preferences.edit().putLong(PREFS_CONFIGURATION, id).apply(); - } - } - - @Override - public void onNothingSelected(final AdapterView parent) { - // do nothing - } - - @Override - public void onNewConfigurationClick() { - // No item has been selected. We must close the spinner manually. - configurationSpinner.close(); - - // Open the dialog - final DialogFragment fragment = UARTNewConfigurationDialogFragment.getInstance(null, false); - fragment.show(getSupportFragmentManager(), null); - - // onNewConfiguration(null, false) will be called when user press OK - } - - @Override - public void onImportClick() { - // No item has been selected. We must close the spinner manually. - configurationSpinner.close(); - - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("text/xml"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - if (intent.resolveActivity(getPackageManager()) != null) { - // file browser has been found on the device - startActivityForResult(intent, SELECT_FILE_REQ); - } else { - // there is no any file browser app, let's try to download one - final View customView = getLayoutInflater().inflate(R.layout.app_file_browser, null); - final ListView appsList = customView.findViewById(android.R.id.list); - appsList.setAdapter(new FileBrowserAppsAdapter(this)); - appsList.setChoiceMode(ListView.CHOICE_MODE_SINGLE); - appsList.setItemChecked(0, true); - new AlertDialog.Builder(this) - .setTitle(R.string.dfu_alert_no_filebrowser_title) - .setView(customView) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss()) - .setPositiveButton(R.string.yes, (dialog, which) -> { - final int pos = appsList.getCheckedItemPosition(); - if (pos >= 0) { - final String query = getResources().getStringArray(R.array.dfu_app_file_browser_action)[pos]; - final Intent storeIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(query)); - startActivity(storeIntent); - } - }) - .show(); - } - } - - @Override - protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - - Uri uri = data != null ? data.getData() : null; - if (resultCode == Activity.RESULT_CANCELED || uri == null) - return; - - switch (requestCode) { - case SELECT_FILE_REQ: { - /* - * The URI returned from application may be in 'file' or 'content' schema. - * 'File' schema allows us to create a File object and read details from if directly. - * Data from 'Content' schema must be read with use of a Content Provider. - * To do that we are using a Loader. - */ - if (uri.getScheme().equals("file")) { - // The direct path to the file has been returned - final String path = uri.getPath(); - try { - final FileInputStream fis = new FileInputStream(path); - loadConfiguration(fis); - } catch (final FileNotFoundException e) { - Toast.makeText(this, R.string.uart_configuration_load_error, Toast.LENGTH_LONG).show(); - } - } else if (uri.getScheme().equals("content")) { - // An Uri has been returned - Uri u = uri; - - // If application returned Uri for streaming, let's us it. Does it works? - final Bundle extras = data.getExtras(); - if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) - u = extras.getParcelable(Intent.EXTRA_STREAM); - - try { - final InputStream is = getContentResolver().openInputStream(u); - loadConfiguration(is); - } catch (final FileNotFoundException e) { - Toast.makeText(this, R.string.uart_configuration_load_error, Toast.LENGTH_LONG).show(); - } - } - break; - } - case REQUEST_SAVE: { - try { - final OutputStream stream = getContentResolver().openOutputStream(uri); - final OutputStreamWriter writer = new OutputStreamWriter(stream); - writer.append(databaseHelper.getConfiguration(configurationSpinner.getSelectedItemId())); - writer.close(); - Toast.makeText(this, R.string.uart_configuration_export_succeeded, Toast.LENGTH_SHORT).show(); - } catch (final Exception e) { - Log.e(TAG, "Error while exporting server configuration", e); - Toast.makeText(this, R.string.uart_configuration_save_error, Toast.LENGTH_SHORT).show(); - } - break; - } - } - } - - public void onCommandChanged(final int index, final String message, final boolean active, final int eol, final int iconIndex) { - final Command command = configuration.getCommands()[index]; - - command.setCommand(message); - command.setActive(active); - command.setEol(eol); - command.setIconIndex(iconIndex); - configurationListener.onConfigurationModified(); - saveConfiguration(); - } - - @Override - public void onNewConfiguration(final String name, final boolean duplicate) { - final boolean exists = databaseHelper.configurationExists(name); - if (exists) { - Toast.makeText(this, R.string.uart_configuration_name_already_taken, Toast.LENGTH_LONG).show(); - return; - } - - UartConfiguration configuration = this.configuration; - if (!duplicate) - configuration = new UartConfiguration(); - configuration.setName(name); - - try { - final Format format = new Format(new HyphenStyle()); - final Strategy strategy = new VisitorStrategy(new CommentVisitor()); - final Serializer serializer = new Persister(strategy, format); - final StringWriter writer = new StringWriter(); - serializer.write(configuration, writer); - final String xml = writer.toString(); - - final long id = databaseHelper.addConfiguration(name, xml); - wearableSynchronizer.onConfigurationAddedOrEdited(id, configuration); - refreshConfigurations(); - selectConfiguration(configurationsAdapter.getItemPosition(id)); - } catch (final Exception e) { - Log.e(TAG, "Error while creating a new configuration", e); - } - } - - @Override - public void onRenameConfiguration(final String newName) { - final boolean exists = databaseHelper.configurationExists(newName); - if (exists) { - Toast.makeText(this, R.string.uart_configuration_name_already_taken, Toast.LENGTH_LONG).show(); - return; - } - - final String oldName = configuration.getName(); - configuration.setName(newName); - - try { - final Format format = new Format(new HyphenStyle()); - final Strategy strategy = new VisitorStrategy(new CommentVisitor()); - final Serializer serializer = new Persister(strategy, format); - final StringWriter writer = new StringWriter(); - serializer.write(configuration, writer); - final String xml = writer.toString(); - - databaseHelper.renameConfiguration(oldName, newName, xml); - wearableSynchronizer.onConfigurationAddedOrEdited(preferences.getLong(PREFS_CONFIGURATION, 0), configuration); - refreshConfigurations(); - } catch (final Exception e) { - Log.e(TAG, "Error while renaming configuration", e); - } - } - - private void refreshConfigurations() { - configurationsAdapter.swapCursor(databaseHelper.getConfigurationsNames()); - configurationsAdapter.notifyDataSetChanged(); - invalidateOptionsMenu(); - } - - private void selectConfiguration(final int position) { - configurationSpinner.setSelection(position); - } - - /** - * Updates the ActionBar background color depending on whether we are in edit mode or not. - * - * @param editMode - * true to show edit mode, false otherwise - * @param change - * if true the background will change with animation, otherwise immediately - */ - @SuppressLint("NewApi") - private void setEditMode(final boolean editMode, final boolean change) { - this.editMode = editMode; - configurationListener.setEditMode(editMode); - if (!change) { - final ColorDrawable color = new ColorDrawable(); - int darkColor = 0; - if (editMode) { - color.setColor(ContextCompat.getColor(this, R.color.orange)); - darkColor = ContextCompat.getColor(this, R.color.dark_orange); - } else { - color.setColor(ContextCompat.getColor(this, R.color.actionBarColor)); - darkColor = ContextCompat.getColor(this, R.color.actionBarColorDark); - } - getSupportActionBar().setBackgroundDrawable(color); - - // Since Lollipop the status bar color may also be changed - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - getWindow().setStatusBarColor(darkColor); - } else { - final TransitionDrawable transition = (TransitionDrawable) getResources().getDrawable( - editMode ? R.drawable.start_edit_mode : R.drawable.stop_edit_mode); - transition.setCrossFadeEnabled(true); - getSupportActionBar().setBackgroundDrawable(transition); - transition.startTransition(200); - - // Since Lollipop the status bar color may also be changed - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - final int colorFrom = ContextCompat.getColor(this, editMode ? R.color.actionBarColorDark : R.color.dark_orange); - final int colorTo = ContextCompat.getColor(this, !editMode ? R.color.actionBarColorDark : R.color.dark_orange); - - final ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); - anim.setDuration(200); - anim.addUpdateListener(animation -> getWindow().setStatusBarColor((Integer) animation.getAnimatedValue())); - anim.start(); - } - - if (slider != null && editMode) { - slider.closePane(); - } - } - } - - /** - * Saves the given configuration in the database. - */ - private void saveConfiguration() { - final UartConfiguration configuration = this.configuration; - try { - final Format format = new Format(new HyphenStyle()); - final Strategy strategy = new VisitorStrategy(new CommentVisitor()); - final Serializer serializer = new Persister(strategy, format); - final StringWriter writer = new StringWriter(); - serializer.write(configuration, writer); - final String xml = writer.toString(); - - databaseHelper.updateConfiguration(configuration.getName(), xml); - wearableSynchronizer.onConfigurationAddedOrEdited(preferences.getLong(PREFS_CONFIGURATION, 0), configuration); - } catch (final Exception e) { - Log.e(TAG, "Error while creating a new configuration", e); - } - } - - /** - * Loads the configuration from the given input stream. - * @param is the input stream - */ - private void loadConfiguration(@NonNull final InputStream is) { - try { - final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - final StringBuilder builder = new StringBuilder(); - for (String line = reader.readLine(); line != null; line = reader.readLine()) { - builder.append(line).append("\n"); - } - final String xml = builder.toString(); - - final Format format = new Format(new HyphenStyle()); - final Serializer serializer = new Persister(format); - final UartConfiguration configuration = serializer.read(UartConfiguration.class, xml); - - final String name = configuration.getName(); - if (!databaseHelper.configurationExists(name)) { - final long id = databaseHelper.addConfiguration(name, xml); - wearableSynchronizer.onConfigurationAddedOrEdited(id, configuration); - refreshConfigurations(); - new Handler().post(() -> selectConfiguration(configurationsAdapter.getItemPosition(id))); - } else { - Toast.makeText(this, R.string.uart_configuration_name_already_taken, Toast.LENGTH_LONG).show(); - } - } catch (final Exception e) { - Log.e(TAG, "Loading configuration failed", e); - - String message; - if (e.getLocalizedMessage() != null) - message = e.getLocalizedMessage(); - else if (e.getCause() != null && e.getCause().getLocalizedMessage() != null) - message = e.getCause().getLocalizedMessage(); - else - message = "Unknown error"; - final String msg = message; - Snackbar.make(container, R.string.uart_configuration_loading_failed, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.uart_action_details, v -> - new AlertDialog.Builder(UARTActivity.this) - .setMessage(msg) - .setTitle(R.string.uart_action_details) - .setPositiveButton(R.string.ok, null) - .show()) - .show(); - } - } - - private void exportConfiguration() { - final String fileName = configuration.getName() + ".xml"; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("text/xml"); - intent.putExtra(Intent.EXTRA_TITLE, fileName); - startActivityForResult(intent, REQUEST_SAVE); - } else { - final File folder = new File(Environment.getExternalStorageDirectory(), FileHelper.NORDIC_FOLDER); - if (!folder.exists()) - folder.mkdir(); - final File serverFolder = new File(folder, FileHelper.UART_FOLDER); - if (!serverFolder.exists()) - serverFolder.mkdir(); - - final File file = new File(serverFolder, fileName); - try { - file.createNewFile(); - final FileOutputStream fos = new FileOutputStream(file); - final OutputStreamWriter writer = new OutputStreamWriter(fos); - writer.append(databaseHelper.getConfiguration(configurationSpinner.getSelectedItemId())); - writer.close(); - - // Notify user about the file - final Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(FileHelper.getContentUri(this, file), "text/xml"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final PendingIntent pendingIntent = PendingIntent.getActivity(this, 420, intent, 0); - final Notification notification = new NotificationCompat.Builder(this, ToolboxApplication.FILE_SAVED_CHANNEL) - .setContentIntent(pendingIntent) - .setContentTitle(fileName) - .setContentText(getText(R.string.uart_configuration_export_succeeded)) - .setAutoCancel(true) - .setShowWhen(true) - .setTicker(getText(R.string.uart_configuration_export_succeeded_ticker)) - .setSmallIcon(android.R.drawable.stat_notify_sdcard) - .build(); - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(fileName, 823, notification); - } catch (final Exception e) { - Log.e(TAG, "Error while exporting configuration", e); - Toast.makeText(this, R.string.uart_configuration_save_error, Toast.LENGTH_SHORT).show(); - } - } - } - - /** - * Converts the old configuration, stored in preferences, into the first XML configuration and saves it to the database. - * If there is already any configuration in the database this method does nothing. - */ - private void ensureFirstConfiguration(@NonNull final DatabaseHelper databaseHelper) { - // This method ensures that the "old", single configuration has been saved to the database. - if (databaseHelper.getConfigurationsCount() == 0) { - final UartConfiguration configuration = new UartConfiguration(); - configuration.setName("First configuration"); - final Command[] commands = configuration.getCommands(); - - for (int i = 0; i < 9; ++i) { - final String cmd = preferences.getString(PREFS_BUTTON_COMMAND + i, null); - if (cmd != null) { - final Command command = new Command(); - command.setCommand(cmd); - command.setActive(preferences.getBoolean(PREFS_BUTTON_ENABLED + i, false)); - command.setEol(0); // default one - command.setIconIndex(preferences.getInt(PREFS_BUTTON_ICON + i, 0)); - commands[i] = command; - } - } - - try { - final Format format = new Format(new HyphenStyle()); - final Strategy strategy = new VisitorStrategy(new CommentVisitor()); - final Serializer serializer = new Persister(strategy, format); - final StringWriter writer = new StringWriter(); - serializer.write(configuration, writer); - final String xml = writer.toString(); - - databaseHelper.addConfiguration(configuration.getName(), xml); - } catch (final Exception e) { - Log.e(TAG, "Error while creating default configuration", e); - } - } - } - - /** - * The comment visitor will add comments to the XML during saving. - */ - private class CommentVisitor implements Visitor { - @Override - public void read(final Type type, final NodeMap node) { - // do nothing - } - - @Override - public void write(final Type type, final NodeMap node) { - if (type.getType().equals(Command[].class)) { - final OutputNode element = node.getNode(); - - final StringBuilder builder = new StringBuilder("A configuration must have 9 commands, one for each button.\n Possible icons are:"); - for (Command.Icon icon : Command.Icon.values()) - builder.append("\n - ").append(icon.toString()); - element.setComment(builder.toString()); - } - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java deleted file mode 100644 index 1190266d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTButtonAdapter.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.uart.domain.Command; -import no.nordicsemi.android.nrftoolbox.uart.domain.UartConfiguration; - -public class UARTButtonAdapter extends BaseAdapter { - private UartConfiguration configuration; - private boolean editMode; - - public UARTButtonAdapter(final UartConfiguration configuration) { - this.configuration = configuration; - } - - public void setEditMode(final boolean editMode) { - this.editMode = editMode; - notifyDataSetChanged(); - } - - public void setConfiguration(final UartConfiguration configuration) { - this.configuration = configuration; - notifyDataSetChanged(); - } - - @Override - public int getCount() { - return configuration != null ? configuration.getCommands().length : 0; - } - - @Override - public Object getItem(final int position) { - return configuration.getCommands()[position]; - } - - @Override - public long getItemId(final int position) { - return position; - } - - @Override - public boolean hasStableIds() { - return true; - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public boolean isEnabled(final int position) { - final Command command = (Command) getItem(position); - return editMode || (command != null && command.isActive()); - } - - @Override - public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) { - View view = convertView; - if (view == null) { - final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - view = inflater.inflate(R.layout.feature_uart_button, parent, false); - } - view.setEnabled(isEnabled(position)); - view.setActivated(editMode); - - // Update image - final Command command = (Command) getItem(position); - final ImageView image = (ImageView) view; - final boolean active = command != null && command.isActive(); - if (active) { - final int icon = command.getIconIndex(); - image.setImageResource(R.drawable.uart_button); - image.setImageLevel(icon); - } else - image.setImageDrawable(null); - - return view; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java deleted file mode 100644 index e7fc685f..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTConfigurationsAdapter.java +++ /dev/null @@ -1,129 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.uart; - -import android.content.Context; -import android.database.Cursor; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import no.nordicsemi.android.nrftoolbox.R; - -public class UARTConfigurationsAdapter extends CursorAdapter { - final Context context; - final ActionListener listener; - - public interface ActionListener { - void onNewConfigurationClick(); - void onImportClick(); - } - - public UARTConfigurationsAdapter(@NonNull final Context context, final ActionListener listener, final Cursor c) { - super(context, c, 0); - this.context = context; - this.listener = listener; - } - - @Override - public int getCount() { - return super.getCount() + 1; // One for buttons at the top - } - - @Override - public boolean isEmpty() { - return super.getCount() == 0; - } - - @Override - public boolean hasStableIds() { - return true; - } - - @Override - public long getItemId(final int position) { - if (position > 0) - return super.getItemId(position - 1); - return 0; - } - - public int getItemPosition(final long id) { - final Cursor cursor = getCursor(); - if (cursor == null) - return 1; - - if (cursor.moveToFirst()) - do { - if (cursor.getLong(0 /* _ID */) == id) - return cursor.getPosition() + 1; - } while (cursor.moveToNext()); - return 1; // should never happen - } - - @Override - public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) { - if (position == 0) { - // This empty view should never be visible. Only positions 1+ are valid. Position 0 is reserved for action buttons. - // It is only created temporally when activity is created. - return LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_spinner_item, parent, false); - } - return super.getView(position - 1, convertView, parent); - } - - @Override - public View getDropDownView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) { - if (position == 0) { - return newToolbarView(context, parent); - } - if (convertView instanceof ViewGroup) - return super.getDropDownView(position - 1, null, parent); - return super.getDropDownView(position - 1, convertView, parent); - } - - @Override - public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { - return LayoutInflater.from(context).inflate(android.R.layout.simple_spinner_item, parent, false); - } - - @Override - public View newDropDownView(final Context context, final Cursor cursor, final ViewGroup parent) { - return LayoutInflater.from(context).inflate(R.layout.feature_uart_dropdown_item, parent, false); - } - - public View newToolbarView(final Context context, final ViewGroup parent) { - final View view = LayoutInflater.from(context).inflate(R.layout.feature_uart_dropdown_title, parent, false); - view.findViewById(R.id.action_add).setOnClickListener(v -> listener.onNewConfigurationClick()); - view.findViewById(R.id.action_import).setOnClickListener(v -> listener.onImportClick()); - return view; - } - - @Override - public void bindView(final View view, final Context context, final Cursor cursor) { - final String name = cursor.getString(1 /* NAME */); - ((TextView) view).setText(name); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java deleted file mode 100644 index ec9bbb1d..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTControlFragment.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.GridView; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.uart.domain.Command; -import no.nordicsemi.android.nrftoolbox.uart.domain.UartConfiguration; - -public class UARTControlFragment extends Fragment implements GridView.OnItemClickListener, UARTActivity.ConfigurationListener { - private final static String TAG = "UARTControlFragment"; - private final static String SIS_EDIT_MODE = "sis_edit_mode"; - - private UartConfiguration configuration; - private UARTButtonAdapter adapter; - private boolean editMode; - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - - try { - ((UARTActivity)context).setConfigurationListener(this); - } catch (final ClassCastException e) { - Log.e(TAG, "The parent activity must implement EditModeListener"); - } - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState != null) { - editMode = savedInstanceState.getBoolean(SIS_EDIT_MODE); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - ((UARTActivity)requireActivity()).setConfigurationListener(null); - } - - @Override - public void onSaveInstanceState(final Bundle outState) { - outState.putBoolean(SIS_EDIT_MODE, editMode); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.fragment_feature_uart_control, container, false); - - final GridView grid = view.findViewById(R.id.grid); - grid.setAdapter(adapter = new UARTButtonAdapter(configuration)); - grid.setOnItemClickListener(this); - adapter.setEditMode(editMode); - - return view; - } - - @Override - public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { - if (editMode) { - Command command = configuration.getCommands()[position]; - if (command == null) - configuration.getCommands()[position] = command = new Command(); - final UARTEditDialog dialog = UARTEditDialog.getInstance(position, command); - dialog.show(getChildFragmentManager(), null); - } else { - final Command command = (Command)adapter.getItem(position); - final Command.Eol eol = command.getEol(); - String text = command.getCommand(); - if (text == null) - text = ""; - switch (eol) { - case CR_LF: - text = text.replaceAll("\n", "\r\n"); - break; - case CR: - text = text.replaceAll("\n", "\r"); - break; - } - final UARTInterface uart = (UARTInterface) requireActivity(); - uart.send(text); - } - } - - @Override - public void onConfigurationModified() { - adapter.notifyDataSetChanged(); - } - - @Override - public void onConfigurationChanged(@NonNull final UartConfiguration configuration) { - this.configuration = configuration; - adapter.setConfiguration(configuration); - } - - @Override - public void setEditMode(final boolean editMode) { - this.editMode = editMode; - adapter.setEditMode(editMode); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java deleted file mode 100644 index 67e104d0..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTEditDialog.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.app.Dialog; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.appcompat.app.AlertDialog; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.GridView; -import android.widget.ImageView; -import android.widget.RadioGroup; - -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.uart.domain.Command; - -public class UARTEditDialog extends DialogFragment implements View.OnClickListener, GridView.OnItemClickListener { - private final static String ARG_INDEX = "index"; - private final static String ARG_COMMAND = "command"; - private final static String ARG_EOL = "eol"; - private final static String ARG_ICON_INDEX = "iconIndex"; - private int activeIcon; - - private EditText field; - private CheckBox activeCheckBox; - private RadioGroup eolGroup; - private IconAdapter iconAdapter; - - public static UARTEditDialog getInstance(final int index, final Command command) { - final UARTEditDialog fragment = new UARTEditDialog(); - - final Bundle args = new Bundle(); - args.putInt(ARG_INDEX, index); - args.putString(ARG_COMMAND, command.getCommand()); - args.putInt(ARG_EOL, command.getEol().index); - args.putInt(ARG_ICON_INDEX, command.getIconIndex()); - fragment.setArguments(args); - - return fragment; - } - - @NonNull - @Override - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final LayoutInflater inflater = LayoutInflater.from(getActivity()); - - // Read button configuration - final Bundle args = requireArguments(); - final int index = args.getInt(ARG_INDEX); - final String command = args.getString(ARG_COMMAND); - final int eol = args.getInt(ARG_EOL); - final int iconIndex = args.getInt(ARG_ICON_INDEX); - final boolean active = true; // change to active by default - activeIcon = iconIndex; - - // Create view - final View view = inflater.inflate(R.layout.feature_uart_dialog_edit, null); - final EditText field = this.field = view.findViewById(R.id.field); - final GridView grid = view.findViewById(R.id.grid); - final CheckBox checkBox = activeCheckBox = view.findViewById(R.id.active); - checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - field.setEnabled(isChecked); - grid.setEnabled(isChecked); - if (iconAdapter != null) - iconAdapter.notifyDataSetChanged(); - }); - - final RadioGroup eolGroup = this.eolGroup = view.findViewById(R.id.uart_eol); - switch (Command.Eol.values()[eol]) { - case CR_LF: - eolGroup.check(R.id.uart_eol_cr_lf); - break; - case CR: - eolGroup.check(R.id.uart_eol_cr); - break; - case LF: - default: - eolGroup.check(R.id.uart_eol_lf); - break; - } - - field.setText(command); - field.setEnabled(active); - checkBox.setChecked(active); - grid.setOnItemClickListener(this); - grid.setEnabled(active); - grid.setAdapter(iconAdapter = new IconAdapter()); - - // As we want to have some validation we can't user the DialogInterface.OnClickListener as it's always dismissing the dialog. - final AlertDialog dialog = new AlertDialog.Builder(requireContext()) - .setCancelable(false) - .setTitle(R.string.uart_edit_title) - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .setView(view) - .show(); - final Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - okButton.setOnClickListener(this); - return dialog; - } - - @Override - public void onClick(final View v) { - final boolean active = activeCheckBox.isChecked(); - final String command = field.getText().toString(); - int eol; - - switch (eolGroup.getCheckedRadioButtonId()) { - case R.id.uart_eol_cr_lf: - eol = Command.Eol.CR_LF.index; - break; - case R.id.uart_eol_cr: - eol = Command.Eol.CR.index; - break; - case R.id.uart_eol_lf: - default: - eol = Command.Eol.LF.index; - break; - } - - // Save values - final Bundle args = requireArguments(); - final int index = args.getInt(ARG_INDEX); - - dismiss(); - final UARTActivity parent = (UARTActivity) requireActivity(); - parent.onCommandChanged(index, command, active, eol, activeIcon); - } - - @Override - public void onItemClick(final AdapterView parent, final View view, final int position, final long id) { - activeIcon = position; - iconAdapter.notifyDataSetChanged(); - } - - private class IconAdapter extends BaseAdapter { - private final int SIZE = 20; - - @Override - public int getCount() { - return SIZE; - } - - @Override - public Object getItem(final int position) { - return position; - } - - @Override - public long getItemId(final int position) { - return position; - } - - @Override - public View getView(final int position, @Nullable final View convertView, @NonNull final ViewGroup parent) { - View view = convertView; - if (view == null) { - view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.feature_uart_dialog_edit_icon, parent, false); - } - final ImageView image = (ImageView) view; - image.setImageLevel(position); - image.setActivated(position == activeIcon && activeCheckBox.isChecked()); - return view; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTInterface.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTInterface.java deleted file mode 100644 index 6619ca57..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTInterface.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - - -public interface UARTInterface { - - void send(final String text); -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLocalLogContentProvider.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLocalLogContentProvider.java deleted file mode 100644 index c196a361..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLocalLogContentProvider.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.net.Uri; - -import no.nordicsemi.android.log.localprovider.LocalLogContentProvider; - -public class UARTLocalLogContentProvider extends LocalLogContentProvider { - /** The authority for the contacts provider. */ - public final static String AUTHORITY = "no.nordicsemi.android.nrftoolbox.uart.log"; - /** A content:// style uri to the authority for the log provider. */ - public final static Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); - - @Override - protected Uri getAuthorityUri() { - return AUTHORITY_URI; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogAdapter.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogAdapter.java deleted file mode 100644 index 0ff40abf..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.content.Context; -import android.database.Cursor; -import android.graphics.Color; -import androidx.annotation.NonNull; -import android.util.SparseIntArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorAdapter; -import android.widget.TextView; - -import java.util.Calendar; - -import no.nordicsemi.android.log.LogContract.Log.Level; -import no.nordicsemi.android.nrftoolbox.R; - -public class UARTLogAdapter extends CursorAdapter { - private static final SparseIntArray colors = new SparseIntArray(); - - static { - colors.put(Level.DEBUG, 0xFF009CDE); - colors.put(Level.VERBOSE, 0xFFB8B056); - colors.put(Level.INFO, Color.BLACK); - colors.put(Level.APPLICATION, 0xFF238C0F); - colors.put(Level.WARNING, 0xFFD77926); - colors.put(Level.ERROR, Color.RED); - } - - UARTLogAdapter(@NonNull final Context context) { - super(context, null, 0); - } - - @Override - public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { - final View view = LayoutInflater.from(context).inflate(R.layout.log_item, parent, false); - - final ViewHolder holder = new ViewHolder(); - holder.time = view.findViewById(R.id.time); - holder.data = view.findViewById(R.id.data); - view.setTag(holder); - return view; - } - - @Override - public void bindView(final View view, final Context context, final Cursor cursor) { - final ViewHolder holder = (ViewHolder) view.getTag(); - final Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(cursor.getLong(1 /* TIME */)); - holder.time.setText(context.getString(R.string.log, calendar)); - - final int level = cursor.getInt(2 /* LEVEL */); - holder.data.setText(cursor.getString(3 /* DATA */)); - holder.data.setTextColor(colors.get(level)); - } - - @Override - public boolean isEnabled(int position) { - return false; - } - - private class ViewHolder { - private TextView time; - private TextView data; - } - -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogFragment.java deleted file mode 100644 index b5e0b2a3..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTLogFragment.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.database.Cursor; -import android.os.Bundle; -import android.os.IBinder; -import androidx.annotation.NonNull; -import androidx.fragment.app.ListFragment; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.CursorAdapter; -import android.widget.EditText; -import android.widget.ListView; - -import no.nordicsemi.android.log.ILogSession; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; - -public class UARTLogFragment extends ListFragment implements LoaderManager.LoaderCallbacks { - private static final String SIS_LOG_SCROLL_POSITION = "sis_scroll_position"; - private static final int LOG_SCROLL_NULL = -1; - private static final int LOG_SCROLLED_TO_BOTTOM = -2; - - private static final int LOG_REQUEST_ID = 1; - private static final String[] LOG_PROJECTION = {LogContract.Log._ID, LogContract.Log.TIME, LogContract.Log.LEVEL, LogContract.Log.DATA}; - - /** - * The service UART interface that may be used to send data to the target. - */ - private UARTInterface uartInterface; - /** - * The adapter used to populate the list with log entries. - */ - private CursorAdapter logAdapter; - /** - * The log session created to log events related with the target device. - */ - private ILogSession logSession; - - private EditText field; - private Button sendButton; - - /** - * The last list view position. - */ - private int logScrollPosition; - - /** - * The receiver that listens for {@link BleProfileService#BROADCAST_CONNECTION_STATE} action. - */ - private final BroadcastReceiver commonBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - // This receiver listens only for the BleProfileService.BROADCAST_CONNECTION_STATE action, no need to check it. - final int state = intent.getIntExtra(BleProfileService.EXTRA_CONNECTION_STATE, BleProfileService.STATE_DISCONNECTED); - - switch (state) { - case BleProfileService.STATE_CONNECTED: { - onDeviceConnected(); - break; - } - case BleProfileService.STATE_DISCONNECTED: { - onDeviceDisconnected(); - break; - } - case BleProfileService.STATE_CONNECTING: - case BleProfileService.STATE_DISCONNECTING: - // current implementation does nothing in this states - default: - // there should be no other actions - break; - } - } - }; - - private ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(final ComponentName name, final IBinder service) { - final UARTService.UARTBinder bleService = (UARTService.UARTBinder) service; - uartInterface = bleService; - logSession = bleService.getLogSession(); - - // Start the loader - if (logSession != null) { - getLoaderManager().restartLoader(LOG_REQUEST_ID, null, UARTLogFragment.this); - } - - // and notify user if device is connected - if (bleService.isConnected()) - onDeviceConnected(); - } - - @Override - public void onServiceDisconnected(final ComponentName name) { - onDeviceDisconnected(); - uartInterface = null; - } - }; - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - LocalBroadcastManager.getInstance(requireContext()).registerReceiver(commonBroadcastReceiver, makeIntentFilter()); - - // Load the last log list view scroll position - if (savedInstanceState != null) { - logScrollPosition = savedInstanceState.getInt(SIS_LOG_SCROLL_POSITION); - } - } - - @Override - public void onStart() { - super.onStart(); - - /* - * If the service has not been started before the following lines will not start it. However, if it's running, the Activity will be bound to it - * and notified via serviceConnection. - */ - final Intent service = new Intent(getActivity(), UARTService.class); - requireActivity().bindService(service, serviceConnection, 0); // we pass 0 as a flag so the service will not be created if not exists - } - - @Override - public void onStop() { - super.onStop(); - - try { - requireActivity().unbindService(serviceConnection); - uartInterface = null; - } catch (final IllegalArgumentException e) { - // do nothing, we were not connected to the sensor - } - } - - @Override - public void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - - // Save the last log list view scroll position - final ListView list = getListView(); - final boolean scrolledToBottom = list.getCount() > 0 && list.getLastVisiblePosition() == list.getCount() - 1; - outState.putInt(SIS_LOG_SCROLL_POSITION, scrolledToBottom ? LOG_SCROLLED_TO_BOTTOM : list.getFirstVisiblePosition()); - } - - @Override - public void onDestroy() { - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(commonBroadcastReceiver); - super.onDestroy(); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.fragment_feature_uart_log, container, false); - - final EditText field = this.field = view.findViewById(R.id.field); - field.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_SEND) { - onSendClicked(); - return true; - } - return false; - }); - - final Button sendButton = this.sendButton = view.findViewById(R.id.action_send); - sendButton.setOnClickListener(v -> onSendClicked()); - return view; - } - - @Override - public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // Create the log adapter, initially with null cursor - logAdapter = new UARTLogAdapter(requireContext()); - setListAdapter(logAdapter); - } - - @NonNull - @Override - public Loader onCreateLoader(final int id, final Bundle args) { - switch (id) { - case LOG_REQUEST_ID: { - return new CursorLoader(requireContext(), logSession.getSessionEntriesUri(), LOG_PROJECTION, null, null, LogContract.Log.TIME); - } - } - throw new UnsupportedOperationException("Could not create loader with ID " + id); - } - - @Override - public void onLoadFinished(@NonNull final Loader loader, final Cursor data) { - // Here we have to restore the old saved scroll position, or scroll to the bottom if before adding new events it was scrolled to the bottom. - final ListView list = getListView(); - final int position = logScrollPosition; - final boolean scrolledToBottom = position == LOG_SCROLLED_TO_BOTTOM || (list.getCount() > 0 && list.getLastVisiblePosition() == list.getCount() - 1); - - logAdapter.swapCursor(data); - - if (position > LOG_SCROLL_NULL) { - list.setSelectionFromTop(position, 0); - } else { - if (scrolledToBottom) - list.setSelection(list.getCount() - 1); - } - logScrollPosition = LOG_SCROLL_NULL; - } - - @Override - public void onLoaderReset(@NonNull final Loader loader) { - logAdapter.swapCursor(null); - } - - private void onSendClicked() { - final String text = field.getText().toString(); - - uartInterface.send(text); - - field.setText(null); - field.requestFocus(); - } - - /** - * Method called when user selected a device on the scanner dialog after the service has been started. - * Here we may bind this fragment to it. - */ - public void onServiceStarted() { - // The service has been started, bind to it - final Intent service = new Intent(getActivity(), UARTService.class); - requireActivity().bindService(service, serviceConnection, 0); - } - - /** - * This method is called when user closes the pane in horizontal orientation. The EditText is no longer visible so we need to close the soft keyboard here. - */ - public void onFragmentHidden() { - final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.hideSoftInputFromWindow(field.getWindowToken(), 0); - } - } - - /** - * Method called when the target device has connected. - */ - protected void onDeviceConnected() { - field.setEnabled(true); - sendButton.setEnabled(true); - } - - /** - * Method called when user disconnected from the target UART device or the connection was lost. - */ - protected void onDeviceDisconnected() { - field.setEnabled(false); - sendButton.setEnabled(false); - } - - private static IntentFilter makeIntentFilter() { - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(BleProfileService.BROADCAST_CONNECTION_STATE); - return intentFilter; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java deleted file mode 100644 index 83b06008..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManager.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattService; -import android.content.Context; -import androidx.annotation.NonNull; -import android.text.TextUtils; - -import java.util.UUID; - -import no.nordicsemi.android.ble.WriteRequest; -import no.nordicsemi.android.log.LogContract; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; - -public class UARTManager extends LoggableBleManager { - /** Nordic UART Service UUID */ - private final static UUID UART_SERVICE_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E"); - /** RX characteristic UUID */ - private final static UUID UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"); - /** TX characteristic UUID */ - private final static UUID UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"); - - private BluetoothGattCharacteristic rxCharacteristic, txCharacteristic; - /** - * A flag indicating whether Long Write can be used. It's set to false if the UART RX - * characteristic has only PROPERTY_WRITE_NO_RESPONSE property and no PROPERTY_WRITE. - * If you set it to false here, it will never use Long Write. - * - * TODO change this flag if you don't want to use Long Write even with Write Request. - */ - private boolean useLongWrite = true; - - UARTManager(final Context context) { - super(context); - } - - @NonNull - @Override - protected BleManagerGattCallback getGattCallback() { - return new UARTManagerGattCallback(); - } - - /** - * BluetoothGatt callbacks for connection/disconnection, service discovery, - * receiving indication, etc. - */ - private class UARTManagerGattCallback extends BleManagerGattCallback { - - @Override - protected void initialize() { - setNotificationCallback(txCharacteristic) - .with((device, data) -> { - final String text = data.getStringValue(0); - log(LogContract.Log.Level.APPLICATION, "\"" + text + "\" received"); - mCallbacks.onDataReceived(device, text); - }); - requestMtu(260).enqueue(); - enableNotifications(txCharacteristic).enqueue(); - } - - @Override - public boolean isRequiredServiceSupported(@NonNull final BluetoothGatt gatt) { - final BluetoothGattService service = gatt.getService(UART_SERVICE_UUID); - if (service != null) { - rxCharacteristic = service.getCharacteristic(UART_RX_CHARACTERISTIC_UUID); - txCharacteristic = service.getCharacteristic(UART_TX_CHARACTERISTIC_UUID); - } - - boolean writeRequest = false; - boolean writeCommand = false; - if (rxCharacteristic != null) { - final int rxProperties = rxCharacteristic.getProperties(); - writeRequest = (rxProperties & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0; - writeCommand = (rxProperties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0; - - // Set the WRITE REQUEST type when the characteristic supports it. - // This will allow to send long write (also if the characteristic support it). - // In case there is no WRITE REQUEST property, this manager will divide texts - // longer then MTU-3 bytes into up to MTU-3 bytes chunks. - if (writeRequest) - rxCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); - else - useLongWrite = false; - } - - return rxCharacteristic != null && txCharacteristic != null && (writeRequest || writeCommand); - } - - @Override - protected void onDeviceDisconnected() { - rxCharacteristic = null; - txCharacteristic = null; - useLongWrite = true; - } - } - - // This has been moved to the service in BleManager v2.0. - /*@Override - protected boolean shouldAutoConnect() { - // We want the connection to be kept - return true; - }*/ - - /** - * Sends the given text to RX characteristic. - * @param text the text to be sent - */ - public void send(final String text) { - // Are we connected? - if (rxCharacteristic == null) - return; - - if (!TextUtils.isEmpty(text)) { - final WriteRequest request = writeCharacteristic(rxCharacteristic, text.getBytes()) - .with((device, data) -> log(LogContract.Log.Level.APPLICATION, - "\"" + data.getStringValue(0) + "\" sent")); - if (!useLongWrite) { - // This will automatically split the long data into MTU-3-byte long packets. - request.split(); - } - request.enqueue(); - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManagerCallbacks.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManagerCallbacks.java deleted file mode 100644 index 1a363957..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTManagerCallbacks.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.bluetooth.BluetoothDevice; - -import androidx.annotation.NonNull; -import no.nordicsemi.android.ble.BleManagerCallbacks; - -public interface UARTManagerCallbacks extends BleManagerCallbacks { - - void onDataReceived(@NonNull final BluetoothDevice device, final String data); - - void onDataSent(@NonNull final BluetoothDevice device, final String data); -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTNewConfigurationDialogFragment.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTNewConfigurationDialogFragment.java deleted file mode 100644 index 35088c7e..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTNewConfigurationDialogFragment.java +++ /dev/null @@ -1,138 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.uart; - -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; -import androidx.appcompat.app.AlertDialog; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Button; -import android.widget.EditText; - -import no.nordicsemi.android.nrftoolbox.R; - -public class UARTNewConfigurationDialogFragment extends DialogFragment implements View.OnClickListener { - private static final String NAME = "name"; - private static final String DUPLICATE = "duplicate"; - - private EditText editText; - - private NewConfigurationDialogListener listener; - - public interface NewConfigurationDialogListener { - /** - * Creates a new configuration with given name. - * @param name the name - * @param duplicate true if configuration is to be duplicated - */ - void onNewConfiguration(final String name, final boolean duplicate); - - /** - * Renames the current configuration with given name. - * @param newName the new name - */ - void onRenameConfiguration(final String newName); - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - - if (context instanceof NewConfigurationDialogListener) { - listener = (NewConfigurationDialogListener) context; - } else { - throw new IllegalArgumentException("The parent activity must implement NewConfigurationDialogListener"); - } - } - - @Override - public void onDetach() { - super.onDetach(); - listener = null; - } - - public static DialogFragment getInstance(final String name, final boolean duplicate) { - final DialogFragment dialog = new UARTNewConfigurationDialogFragment(); - - final Bundle args = new Bundle(); - args.putString(NAME, name); - args.putBoolean(DUPLICATE, duplicate); - dialog.setArguments(args); - - return dialog; - } - - @Override - @NonNull - public Dialog onCreateDialog(final Bundle savedInstanceState) { - final Bundle args = requireArguments(); - final String oldName = args.getString(NAME); - final boolean duplicate = args.getBoolean(DUPLICATE); - final int titleResId = duplicate || oldName == null ? R.string.uart_new_configuration_title : R.string.uart_rename_configuration_title; - - final LayoutInflater inflater = LayoutInflater.from(requireContext()); - final View view = inflater.inflate(R.layout.feature_uart_dialog_new_configuration, null); - final EditText editText = this.editText = view.findViewById(R.id.name); - editText.setText(args.getString(NAME)); - final View actionClear = view.findViewById(R.id.action_clear); - actionClear.setOnClickListener(v -> editText.setText(null)); - - final AlertDialog dialog = new AlertDialog.Builder(requireContext()) - .setTitle(titleResId) - .setView(view) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.ok, null) - .setCancelable(false) - .show(); // this must be show() or the getButton() below will return null. - - final Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - okButton.setOnClickListener(this); - - return dialog; - } - - @Override - public void onClick(final View v) { - final String newName = editText.getText().toString().trim(); - if (TextUtils.isEmpty(newName)) { - editText.setError(getString(R.string.uart_empty_name_error)); - return; - } - - final Bundle args = requireArguments(); - final String oldName = args.getString(NAME); - final boolean duplicate = args.getBoolean(DUPLICATE); - - if (duplicate || TextUtils.isEmpty(oldName)) - listener.onNewConfiguration(newName, duplicate); - else { - listener.onRenameConfiguration(newName); - } - dismiss(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java deleted file mode 100644 index be39cca5..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/UARTService.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.bluetooth.BluetoothDevice; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.text.TextUtils; -import android.util.Log; - -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.wearable.MessageApi; -import com.google.android.gms.wearable.Node; -import com.google.android.gms.wearable.NodeApi; -import com.google.android.gms.wearable.Wearable; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import no.nordicsemi.android.log.Logger; -import no.nordicsemi.android.nrftoolbox.FeaturesActivity; -import no.nordicsemi.android.nrftoolbox.R; -import no.nordicsemi.android.nrftoolbox.ToolboxApplication; -import no.nordicsemi.android.nrftoolbox.profile.BleProfileService; -import no.nordicsemi.android.nrftoolbox.profile.LoggableBleManager; -import no.nordicsemi.android.nrftoolbox.wearable.common.Constants; - -public class UARTService extends BleProfileService implements UARTManagerCallbacks { - private static final String TAG = "UARTService"; - - public static final String BROADCAST_UART_TX = "no.nordicsemi.android.nrftoolbox.uart.BROADCAST_UART_TX"; - public static final String BROADCAST_UART_RX = "no.nordicsemi.android.nrftoolbox.uart.BROADCAST_UART_RX"; - public static final String EXTRA_DATA = "no.nordicsemi.android.nrftoolbox.uart.EXTRA_DATA"; - - /** - * A broadcast message with this action and the message in {@link Intent#EXTRA_TEXT} will be sent t the UART device. - */ - public final static String ACTION_SEND = "no.nordicsemi.android.nrftoolbox.uart.ACTION_SEND"; - /** - * A broadcast message with this action is triggered when a message is received from the UART device. - */ - private final static String ACTION_RECEIVE = "no.nordicsemi.android.nrftoolbox.uart.ACTION_RECEIVE"; - /** - * Action send when user press the DISCONNECT button on the notification. - */ - public final static String ACTION_DISCONNECT = "no.nordicsemi.android.nrftoolbox.uart.ACTION_DISCONNECT"; - /** - * A source of an action. - */ - public final static String EXTRA_SOURCE = "no.nordicsemi.android.nrftoolbox.uart.EXTRA_SOURCE"; - public final static int SOURCE_NOTIFICATION = 0; - public final static int SOURCE_WEARABLE = 1; - public final static int SOURCE_3RD_PARTY = 2; - - private final static int NOTIFICATION_ID = 349; // random - private final static int OPEN_ACTIVITY_REQ = 67; // random - private final static int DISCONNECT_REQ = 97; // random - - private GoogleApiClient googleApiClient; - private UARTManager manager; - - private final LocalBinder binder = new UARTBinder(); - - public class UARTBinder extends LocalBinder implements UARTInterface { - @Override - public void send(final String text) { - manager.send(text); - } - } - - @Override - protected LocalBinder getBinder() { - return binder; - } - - @Override - protected LoggableBleManager initializeManager() { - return manager = new UARTManager(this); - } - - @Override - protected boolean shouldAutoConnect() { - return true; - } - - @Override - public void onCreate() { - super.onCreate(); - - registerReceiver(disconnectActionBroadcastReceiver, new IntentFilter(ACTION_DISCONNECT)); - registerReceiver(intentBroadcastReceiver, new IntentFilter(ACTION_SEND)); - - googleApiClient = new GoogleApiClient.Builder(this) - .addApi(Wearable.API) - .build(); - googleApiClient.connect(); - } - - @Override - public void onDestroy() { - // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - stopForegroundService(); - unregisterReceiver(disconnectActionBroadcastReceiver); - unregisterReceiver(intentBroadcastReceiver); - - googleApiClient.disconnect(); - - super.onDestroy(); - } - - @Override - protected void onRebind() { - stopForegroundService(); - } - - @Override - protected void onUnbind() { - startForegroundService(); - } - - @Override - public void onDeviceConnected(@NonNull final BluetoothDevice device) { - super.onDeviceConnected(device); - sendMessageToWearables(Constants.UART.DEVICE_CONNECTED, notNull(getDeviceName())); - } - - @Override - protected boolean stopWhenDisconnected() { - return false; - } - - @Override - public void onDeviceDisconnected(@NonNull final BluetoothDevice device) { - super.onDeviceDisconnected(device); - sendMessageToWearables(Constants.UART.DEVICE_DISCONNECTED, notNull(getDeviceName())); - } - - @Override - public void onLinkLossOccurred(@NonNull final BluetoothDevice device) { - super.onLinkLossOccurred(device); - sendMessageToWearables(Constants.UART.DEVICE_LINKLOSS, notNull(getDeviceName())); - } - - private String notNull(final String name) { - if (!TextUtils.isEmpty(name)) - return name; - return getString(R.string.not_available); - } - - @Override - public void onDataReceived(@NonNull final BluetoothDevice device, final String data) { - final Intent broadcast = new Intent(BROADCAST_UART_RX); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, data); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - - // send the data received to other apps, e.g. the Tasker - final Intent globalBroadcast = new Intent(ACTION_RECEIVE); - globalBroadcast.putExtra(BluetoothDevice.EXTRA_DEVICE, getBluetoothDevice()); - globalBroadcast.putExtra(Intent.EXTRA_TEXT, data); - sendBroadcast(globalBroadcast); - } - - @Override - public void onDataSent(@NonNull final BluetoothDevice device, final String data) { - final Intent broadcast = new Intent(BROADCAST_UART_TX); - broadcast.putExtra(EXTRA_DEVICE, getBluetoothDevice()); - broadcast.putExtra(EXTRA_DATA, data); - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast); - } - - /** - * Sends the given message to all connected wearables. If the path is equal to {@link Constants.UART#DEVICE_DISCONNECTED} the service will be stopped afterwards. - * - * @param path message path - * @param message the message - */ - private void sendMessageToWearables(@NonNull final String path, @NonNull final String message) { - if (googleApiClient.isConnected()) { - new Thread(() -> { - NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); - for (Node node : nodes.getNodes()) { - Logger.v(getLogSession(), "[WEAR] Sending message '" + path + "' to " + node.getDisplayName()); - final MessageApi.SendMessageResult result = Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), path, message.getBytes()).await(); - if (result.getStatus().isSuccess()) { - Logger.i(getLogSession(), "[WEAR] Message sent"); - } else { - Logger.w(getLogSession(), "[WEAR] Sending message failed: " + result.getStatus().getStatusMessage()); - Log.w(TAG, "Failed to send " + path + " to " + node.getDisplayName()); - } - } - if (Constants.UART.DEVICE_DISCONNECTED.equals(path)) - stopService(); - }).start(); - } else { - if (Constants.UART.DEVICE_DISCONNECTED.equals(path)) - stopService(); - } - } - - /** - * Sets the service as a foreground service - */ - private void startForegroundService() { - // when the activity closes we need to show the notification that user is connected to the peripheral sensor - // We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services - final Notification notification = createNotification(R.string.uart_notification_connected_message, 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForeground(NOTIFICATION_ID, notification); - } else { - final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, notification); - } - } - - /** - * Stops the service as a foreground service - */ - private void 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) { - stopForeground(true); - } else { - cancelNotification(); - } - } - - /** - * Creates the notification - * - * @param messageResId message resource id. The message must have one String parameter,
- * f.e. <string name="name">%s is connected</string> - * @param defaults signals that will be used to notify the user - */ - @SuppressWarnings("SameParameterValue") - protected Notification createNotification(final int messageResId, final int defaults) { - final Intent parentIntent = new Intent(this, FeaturesActivity.class); - parentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - final Intent targetIntent = new Intent(this, UARTActivity.class); - - final Intent disconnect = new Intent(ACTION_DISCONNECT); - disconnect.putExtra(EXTRA_SOURCE, SOURCE_NOTIFICATION); - final PendingIntent disconnectAction = PendingIntent.getBroadcast(this, DISCONNECT_REQ, disconnect, PendingIntent.FLAG_UPDATE_CURRENT); - - // both activities above have launchMode="singleTask" in the AndroidManifest.xml file, so if the task is already running, it will be resumed - final PendingIntent pendingIntent = PendingIntent.getActivities(this, OPEN_ACTIVITY_REQ, new Intent[]{parentIntent, targetIntent}, PendingIntent.FLAG_UPDATE_CURRENT); - final NotificationCompat.Builder builder = new NotificationCompat.Builder(this, ToolboxApplication.CONNECTED_DEVICE_CHANNEL); - builder.setContentIntent(pendingIntent); - builder.setContentTitle(getString(R.string.app_name)).setContentText(getString(messageResId, getDeviceName())); - builder.setSmallIcon(R.drawable.ic_stat_notify_uart); - builder.setShowWhen(defaults != 0).setDefaults(defaults).setAutoCancel(true).setOngoing(true); - builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_bluetooth, getString(R.string.uart_notification_action_disconnect), disconnectAction)); - - return builder.build(); - } - - /** - * Cancels the existing notification. If there is no active notification this method does nothing - */ - private void cancelNotification() { - final NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - } - - /** - * This broadcast receiver listens for {@link #ACTION_DISCONNECT} that may be fired by pressing Disconnect action button on the notification. - */ - private final BroadcastReceiver disconnectActionBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final int source = intent.getIntExtra(EXTRA_SOURCE, SOURCE_NOTIFICATION); - switch (source) { - case SOURCE_NOTIFICATION: - Logger.i(getLogSession(), "[Notification] Disconnect action pressed"); - break; - case SOURCE_WEARABLE: - Logger.i(getLogSession(), "[WEAR] '" + Constants.ACTION_DISCONNECT + "' message received"); - break; - } - if (isConnected()) - getBinder().disconnect(); - else - stopSelf(); - } - }; - - /** - * Broadcast receiver that listens for {@link #ACTION_SEND} from other apps. Sends the String or int content of the {@link Intent#EXTRA_TEXT} extra to the remote device. - * The integer content will be sent as String (65 -> "65", not 65 -> "A"). - */ - private BroadcastReceiver intentBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(final Context context, final Intent intent) { - final boolean hasMessage = intent.hasExtra(Intent.EXTRA_TEXT); - if (hasMessage) { - String message = intent.getStringExtra(Intent.EXTRA_TEXT); - if (message == null) { - final int intValue = intent.getIntExtra(Intent.EXTRA_TEXT, Integer.MIN_VALUE); // how big is the chance of such data? - if (intValue != Integer.MIN_VALUE) - message = String.valueOf(intValue); - } - - if (message != null) { - final int source = intent.getIntExtra(EXTRA_SOURCE, SOURCE_3RD_PARTY); - switch (source) { - case SOURCE_WEARABLE: - Logger.i(getLogSession(), "[WEAR] '" + Constants.UART.COMMAND + "' message received with data: \"" + message + "\""); - break; - case SOURCE_3RD_PARTY: - default: - Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received with data: \"" + message + "\""); - break; - } - manager.send(message); - return; - } - } - // No data od incompatible type of EXTRA_TEXT - if (!hasMessage) - Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received no data."); - else - Logger.i(getLogSession(), "[Broadcast] " + ACTION_SEND + " broadcast received incompatible data type. Only String and int are supported."); - } - }; -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/ConfigurationContract.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/ConfigurationContract.java deleted file mode 100644 index c0a40d1b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/ConfigurationContract.java +++ /dev/null @@ -1,38 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.uart.database; - -import android.provider.BaseColumns; - -public class ConfigurationContract { - - protected interface ConfigurationColumns { - /** The XML with configuration. */ - String XML = "xml"; - } - - public final class Configuration implements BaseColumns, NameColumns, ConfigurationColumns, UndoColumns { - private Configuration() { - // empty - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/DatabaseHelper.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/DatabaseHelper.java deleted file mode 100644 index 233c5e58..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/DatabaseHelper.java +++ /dev/null @@ -1,264 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.uart.database; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.provider.BaseColumns; - -@SuppressWarnings("UnusedReturnValue") -public class DatabaseHelper { - /** Database file name */ - private static final String DATABASE_NAME = "toolbox_uart.db"; - /** Database version */ - private static final int DATABASE_VERSION = 1; - - private interface Tables { - /** Configurations table. See {@link ConfigurationContract.Configuration} for column names. */ - String CONFIGURATIONS = "configurations"; - } - - private static final String[] ID_PROJECTION = new String[] { BaseColumns._ID }; - private static final String[] NAME_PROJECTION = new String[] { BaseColumns._ID, NameColumns.NAME }; - private static final String[] XML_PROJECTION = new String[] { - BaseColumns._ID, ConfigurationContract.Configuration.XML - }; - private static final String[] CONFIGURATION_PROJECTION = new String[] { - BaseColumns._ID, NameColumns.NAME, ConfigurationContract.Configuration.XML - }; - - private static final String ID_SELECTION = BaseColumns._ID + "=?"; - private static final String NAME_SELECTION = NameColumns.NAME + "=?"; - private static final String DELETED_SELECTION = UndoColumns.DELETED + "=1"; - private static final String NOT_DELETED_SELECTION = UndoColumns.DELETED + "=0"; - - private static SQLiteHelper databaseHelper; - private static SQLiteDatabase database; - private final ContentValues values = new ContentValues(); - private final String[] singleArg = new String[1]; - - public DatabaseHelper(final Context context) { - if (databaseHelper == null) { - databaseHelper = new SQLiteHelper(context); - database = databaseHelper.getWritableDatabase(); - } - } - - /** - * Returns number of saved configurations. - */ - public int getConfigurationsCount() { - try (Cursor cursor = database.query(Tables.CONFIGURATIONS, ID_PROJECTION, NOT_DELETED_SELECTION, - null, null, null, null)) { - return cursor.getCount(); - } - } - - /** - * Returns the list of all saved configurations. - * @return cursor - */ - public Cursor getConfigurations() { - return database.query(Tables.CONFIGURATIONS, CONFIGURATION_PROJECTION, NOT_DELETED_SELECTION, - null, null, null, ConfigurationContract.Configuration.NAME + " ASC"); - } - - /** - * Returns the list of names of all saved configurations. - * - * @return cursor - */ - public Cursor getConfigurationsNames() { - return database.query(Tables.CONFIGURATIONS, NAME_PROJECTION, NOT_DELETED_SELECTION, - null, null, null, ConfigurationContract.Configuration.NAME + " ASC"); - } - - /** - * Returns the XML wth the configuration by id. - * - * @param id the configuration id in the DB - * @return the XML with configuration or null - */ - public String getConfiguration(final long id) { - singleArg[0] = String.valueOf(id); - - try (Cursor cursor = database.query(Tables.CONFIGURATIONS, XML_PROJECTION, ID_SELECTION, - singleArg, null, null, null)) { - if (cursor.moveToNext()) - return cursor.getString(1 /* XML */); - return null; - } - } - - /** - * Adds new configuration to the database. - * - * @param name the configuration name - * @param configuration the XML - * @return the id or -1 if error occurred - */ - public long addConfiguration(final String name, final String configuration) { - final ContentValues values = this.values; - values.clear(); - values.put(ConfigurationContract.Configuration.NAME, name); - values.put(ConfigurationContract.Configuration.XML, configuration); - values.put(ConfigurationContract.Configuration.DELETED, 0); - return database.replace(Tables.CONFIGURATIONS, null, values); - } - - /** - * Updates the configuration with the given name with the new XML. - * - * @param name the configuration name to be updated - * @param configuration the new XML with configuration - * @return number of rows updated - */ - public int updateConfiguration(final String name, final String configuration) { - singleArg[0] = name; - - final ContentValues values = this.values; - values.clear(); - values.put(ConfigurationContract.Configuration.XML, configuration); - values.put(ConfigurationContract.Configuration.DELETED, 0); - return database.update(Tables.CONFIGURATIONS, values, NAME_SELECTION, singleArg); - } - - /** - * Marks the configuration with given name as deleted. If may be restored or removed permanently - * afterwards. - * - * @param name the configuration name - * @return id of the deleted configuration - */ - public long deleteConfiguration(final String name) { - singleArg[0] = name; - - final ContentValues values = this.values; - values.clear(); - values.put(ConfigurationContract.Configuration.DELETED, 1); - database.update(Tables.CONFIGURATIONS, values, NAME_SELECTION, singleArg); - - try (Cursor cursor = database.query(Tables.CONFIGURATIONS, ID_PROJECTION, NAME_SELECTION, - singleArg, null, null, null)) { - if (cursor.moveToNext()) - return cursor.getLong(0 /* _ID */); - return -1; - } - } - - public int removeDeletedServerConfigurations() { - return database.delete(Tables.CONFIGURATIONS, DELETED_SELECTION, null); - } - - /** - * Restores deleted configuration. Returns the ID of the first one. - * @return the DI of the restored configuration. - */ - public long restoreDeletedServerConfiguration(final String name) { - singleArg[0] = name; - - final ContentValues values = this.values; - values.clear(); - values.put(ConfigurationContract.Configuration.DELETED, 0); - database.update(Tables.CONFIGURATIONS, values, NAME_SELECTION, singleArg); - - try (Cursor cursor = database.query(Tables.CONFIGURATIONS, ID_PROJECTION, NAME_SELECTION, singleArg, - null, null, null)) { - if (cursor.moveToNext()) - return cursor.getLong(0 /* _ID */); - return -1; - } - } - - /** - * Renames the server configuration and replaces its XML (name inside has changed). - * @param oldName the old name to look for - * @param newName the new configuration name - * @param configuration the new XML - * @return number of rows affected - */ - public int renameConfiguration(final String oldName, final String newName, final String configuration) { - singleArg[0] = oldName; - - final ContentValues values = this.values; - values.clear(); - values.put(ConfigurationContract.Configuration.NAME, newName); - values.put(ConfigurationContract.Configuration.XML, configuration); - return database.update(Tables.CONFIGURATIONS, values, NAME_SELECTION, singleArg); - } - - /** - * Returns true if a configuration with given name was found in the database. - * @param name the name to check - * @return true if such name exists, false otherwise - */ - public boolean configurationExists(final String name) { - singleArg[0] = name; - - try (Cursor cursor = database.query(Tables.CONFIGURATIONS, NAME_PROJECTION, NAME_SELECTION - + " AND " + NOT_DELETED_SELECTION, singleArg, null, null, null)) { - return cursor.getCount() > 0; - } - } - - private class SQLiteHelper extends SQLiteOpenHelper { - - /** - * The SQL code that creates the Server Configurations: - * - *
-		 * ----------------------------------------------------------------------------
-		 *                            CONFIGURATIONS                           |
-		 * ----------------------------------------------------------------------------
-		 * | _id (int, pk, auto increment) | name (text) | xml (text) | deleted (int) |
-		 * ----------------------------------------------------------------------------
-		 * 
- */ - private static final String CREATE_CONFIGURATIONS = "CREATE TABLE " + Tables.CONFIGURATIONS - + "(" + ConfigurationContract.Configuration._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " - + ConfigurationContract.Configuration.NAME + " TEXT UNIQUE NOT NULL, " - + ConfigurationContract.Configuration.XML + " TEXT NOT NULL, " - + ConfigurationContract.Configuration.DELETED +" INTEGER NOT NULL DEFAULT(0))"; - - SQLiteHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(final SQLiteDatabase db) { - db.execSQL(CREATE_CONFIGURATIONS); - } - - @Override - public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { - // This method does nothing for now. - //noinspection SwitchStatementWithTooFewBranches - switch (oldVersion) { - case 1: - // do nothing - } - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/NameColumns.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/NameColumns.java deleted file mode 100644 index 65aba510..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/NameColumns.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart.database; - -public interface NameColumns { - /** The name */ - String NAME = "name"; -} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/UndoColumns.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/UndoColumns.java deleted file mode 100644 index 12ff8986..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/database/UndoColumns.java +++ /dev/null @@ -1,27 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.uart.database; - -public interface UndoColumns { - /** The 'deleted' flag */ - String DELETED = "deleted"; -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java deleted file mode 100644 index e364c551..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/Command.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart.domain; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Root; -import org.simpleframework.xml.Text; - -@Root -public class Command { - public enum Icon { - 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); - - public final int index; - - Icon(final int index) { - this.index = index; - } - } - - public enum Eol { - LF(0), - CR(1), - CR_LF(2); - - public final int index; - - Eol(final int index) { - this.index = index; - } - } - - @Text(required = false) - private String command; - - @Attribute(required = false) - private boolean active = false; - - @Attribute(required = false) - private Eol eol = Eol.LF; - - @Attribute(required = false) - private Icon icon = Icon.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 = Eol.values()[eol]; - } - - /** - * Sets the icon index. - * @param index index of the icon. - */ - public void setIconIndex(final int index) { - this.icon = Icon.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 Eol getEol() { - return eol; - } - - /** - * Returns the icon index. - * @return the icon index - */ - public int getIconIndex() { - return icon.index; - } - /** - * Returns the EOL index. - * @return the EOL index - */ - public int getEolIndex() { - return eol.index; - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java deleted file mode 100644 index 981f48ce..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/domain/UartConfiguration.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart.domain; - -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 UartConfiguration { - public static final int COMMANDS_COUNT = 9; - - @Attribute(required = false, empty = "Unnamed") - private String name; - - @ElementArray - private Command[] commands = new Command[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 Command[] getCommands() { - return 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/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/wearable/UARTConfigurationSynchronizer.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/wearable/UARTConfigurationSynchronizer.java deleted file mode 100644 index 095e49ad..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/uart/wearable/UARTConfigurationSynchronizer.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2015, 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.uart.wearable; - -import android.content.Context; -import android.net.Uri; - -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.PendingResult; -import com.google.android.gms.wearable.DataApi; -import com.google.android.gms.wearable.DataMap; -import com.google.android.gms.wearable.PutDataMapRequest; -import com.google.android.gms.wearable.PutDataRequest; -import com.google.android.gms.wearable.Wearable; - -import java.util.ArrayList; - -import no.nordicsemi.android.nrftoolbox.uart.domain.Command; -import no.nordicsemi.android.nrftoolbox.uart.domain.UartConfiguration; -import no.nordicsemi.android.nrftoolbox.wearable.common.Constants; - -public class UARTConfigurationSynchronizer { - private static final String WEAR_URI_PREFIX = "wear:"; // no / at the end as the path already has it - - private static UARTConfigurationSynchronizer instance; - private GoogleApiClient googleApiClient; - - /** - * Initializes the synchronizer. - * @param context the activity context - * @param listener the connection callbacks listener - */ - public static UARTConfigurationSynchronizer from(final Context context, final GoogleApiClient.ConnectionCallbacks listener) { - if (instance == null) - instance = new UARTConfigurationSynchronizer(); - - instance.init(context, listener); - return instance; - } - - private UARTConfigurationSynchronizer() { - // private constructor - } - - private void init(final Context context, final GoogleApiClient.ConnectionCallbacks listener) { - if (googleApiClient != null) - return; - - googleApiClient = new GoogleApiClient.Builder(context) - .addApiIfAvailable(Wearable.API) - .addConnectionCallbacks(listener) - .build(); - googleApiClient.connect(); - } - - /** - * Closes the synchronizer. - */ - public void close() { - if (googleApiClient != null) - googleApiClient.disconnect(); - googleApiClient = null; - } - - /** - * Returns true if Wearable API has been connected. - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean hasConnectedApi() { - return googleApiClient != null && googleApiClient.isConnected() && googleApiClient.hasConnectedApi(Wearable.API); - } - - /** - * Synchronizes the UART configurations between handheld and wearables. - * Call this when configuration has been created or altered. - * @return pending result - */ - public PendingResult onConfigurationAddedOrEdited(final long id, final UartConfiguration configuration) { - if (!hasConnectedApi()) - return null; - - final PutDataMapRequest mapRequest = PutDataMapRequest.create(Constants.UART.CONFIGURATIONS + "/" + id); - final DataMap map = mapRequest.getDataMap(); - map.putString(Constants.UART.Configuration.NAME, configuration.getName()); - final ArrayList commands = new ArrayList<>(UartConfiguration.COMMANDS_COUNT); - for (Command command : configuration.getCommands()) { - if (command != null && command.isActive()) { - final DataMap item = new DataMap(); - item.putInt(Constants.UART.Configuration.Command.ICON_ID, command.getIconIndex()); - item.putString(Constants.UART.Configuration.Command.MESSAGE, command.getCommand()); - item.putInt(Constants.UART.Configuration.Command.EOL, command.getEolIndex()); - commands.add(item); - } - } - map.putDataMapArrayList(Constants.UART.Configuration.COMMANDS, commands); - final PutDataRequest request = mapRequest.asPutDataRequest(); - return Wearable.DataApi.putDataItem(googleApiClient, request); - } - - /** - * Synchronizes the UART configurations between handheld and wearables. - * Call this when configuration has been deleted. - * @return pending result - */ - @SuppressWarnings("UnusedReturnValue") - public PendingResult onConfigurationDeleted(final long id) { - if (!hasConnectedApi()) - return null; - return Wearable.DataApi.deleteDataItems(googleApiClient, id2Uri(id)); - } - - /** - * Creates URI without nodeId. - * @param id the configuration id in the database - * @return Uri that may be used to delete the associated DataMap. - */ - private Uri id2Uri(final long id) { - return Uri.parse(WEAR_URI_PREFIX + Constants.UART.CONFIGURATIONS + "/" + id); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Color.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Color.kt new file mode 100644 index 00000000..5fa89076 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Color.kt @@ -0,0 +1,8 @@ +package no.nordicsemi.android.nrftoolbox.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Shape.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Shape.kt new file mode 100644 index 00000000..6f6167bd --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.nrftoolbox.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Theme.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Theme.kt new file mode 100644 index 00000000..c0218240 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Theme.kt @@ -0,0 +1,44 @@ +package no.nordicsemi.android.nrftoolbox.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun TestTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Type.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Type.kt new file mode 100644 index 00000000..51752962 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ui/theme/Type.kt @@ -0,0 +1,28 @@ +package no.nordicsemi.android.nrftoolbox.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/FileHelper.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/FileHelper.java deleted file mode 100644 index 636941c8..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/FileHelper.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2015, 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.utility; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.provider.BaseColumns; -import android.provider.MediaStore; - -import java.io.File; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public class FileHelper { - - public static final String NORDIC_FOLDER = "Nordic Semiconductor"; - public static final String UART_FOLDER = "UART Configurations"; - - @Nullable - public static Uri getContentUri(@NonNull final Context context, @NonNull final File file) { - final String filePath = file.getAbsolutePath(); - final Uri uri = MediaStore.Files.getContentUri("external"); - try (final Cursor cursor = context.getContentResolver().query( - uri, - new String[] { BaseColumns._ID }, - MediaStore.Files.FileColumns.DATA + "=? ", - new String[] { filePath }, - null)) { - if (cursor != null && cursor.moveToFirst()) { - final int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); - return Uri.withAppendedPath(uri, String.valueOf(id)); - } else { - if (file.exists()) { - final ContentValues values = new ContentValues(); - values.put(MediaStore.Files.FileColumns.DATA, filePath); - return context.getContentResolver().insert(uri, values); - } else { - return null; - } - } - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/ParserUtils.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/ParserUtils.java deleted file mode 100644 index 5e11b143..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/utility/ParserUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2015, 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.utility; - -public class ParserUtils extends no.nordicsemi.android.ble.utils.ParserUtils { - - public static String parseDebug(final byte[] data) { - if (data == null || data.length == 0) - return ""; - - final char[] out = new char[data.length * 2]; - for (int j = 0; j < data.length; j++) { - int v = data[j] & 0xFF; - out[j * 2] = HEX_ARRAY[v >>> 4]; - out[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return "0x" + new String(out); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/wearable/MainWearableListenerService.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/wearable/MainWearableListenerService.java deleted file mode 100644 index 62cf12d6..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/wearable/MainWearableListenerService.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2015, 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.wearable; - -import android.content.Intent; - -import com.google.android.gms.wearable.MessageEvent; -import com.google.android.gms.wearable.WearableListenerService; - -import no.nordicsemi.android.nrftoolbox.uart.UARTService; -import no.nordicsemi.android.nrftoolbox.wearable.common.Constants; - -/** - * The main listener for messages from Wearable devices. There may be only one such service per application so it has to handle messages from all profiles. - */ -public class MainWearableListenerService extends WearableListenerService { - - @Override - public void onMessageReceived(final MessageEvent messageEvent) { - switch (messageEvent.getPath()) { - case Constants.ACTION_DISCONNECT: { - // A disconnect message was sent. The information which profile should be disconnected is in the data. - final String profile = new String(messageEvent.getData()); - - //noinspection SwitchStatementWithTooFewBranches - switch (profile) { - // Currently only UART profile has Wear support - case Constants.UART.PROFILE: { - final Intent disconnectIntent = new Intent(UARTService.ACTION_DISCONNECT); - disconnectIntent.putExtra(UARTService.EXTRA_SOURCE, UARTService.SOURCE_WEARABLE); - sendBroadcast(disconnectIntent); - break; - } - } - break; - } - case Constants.UART.COMMAND: { - final String command = new String(messageEvent.getData()); - - final Intent intent = new Intent(UARTService.ACTION_SEND); - intent.putExtra(UARTService.EXTRA_SOURCE, UARTService.SOURCE_WEARABLE); - intent.putExtra(Intent.EXTRA_TEXT, command); - sendBroadcast(intent); - } - default: - super.onMessageReceived(messageEvent); - break; - } - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ClosableSpinner.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ClosableSpinner.java deleted file mode 100644 index baf19ed4..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ClosableSpinner.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2015, 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.widget; - -import android.content.Context; -import android.util.AttributeSet; - -public class ClosableSpinner extends androidx.appcompat.widget.AppCompatSpinner { - public ClosableSpinner(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public void close() { - super.onDetachedFromWindow(); - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/DividerItemDecoration.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/DividerItemDecoration.java deleted file mode 100644 index ad28e438..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/DividerItemDecoration.java +++ /dev/null @@ -1,111 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.widget; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.view.View; - -public class DividerItemDecoration extends RecyclerView.ItemDecoration { - - private static final int[] ATTRS = new int[]{ - android.R.attr.listDivider - }; - - public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; - - public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; - - private Drawable divider; - - private int orientation; - - public DividerItemDecoration(Context context, int orientation) { - final TypedArray a = context.obtainStyledAttributes(ATTRS); - divider = a.getDrawable(0); - a.recycle(); - setOrientation(orientation); - } - - public void setOrientation(int orientation) { - if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { - throw new IllegalArgumentException("invalid orientation"); - } - orientation = orientation; - } - - @Override - public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { - if (orientation == VERTICAL_LIST) { - drawVertical(c, parent); - } else { - drawHorizontal(c, parent); - } - } - - public void drawVertical(Canvas c, RecyclerView parent) { - final int left = parent.getPaddingLeft(); - final int right = parent.getWidth() - parent.getPaddingRight(); - - final int childCount = parent.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = parent.getChildAt(i); - final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child - .getLayoutParams(); - final int top = child.getBottom() + params.bottomMargin; - final int bottom = top + divider.getIntrinsicHeight(); - divider.setBounds(left, top, right, bottom); - divider.draw(c); - } - } - - public void drawHorizontal(Canvas c, RecyclerView parent) { - final int top = parent.getPaddingTop(); - final int bottom = parent.getHeight() - parent.getPaddingBottom(); - - final int childCount = parent.getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = parent.getChildAt(i); - final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child - .getLayoutParams(); - final int left = child.getRight() + params.rightMargin; - final int right = left + divider.getIntrinsicHeight(); - divider.setBounds(left, top, right, bottom); - divider.draw(c); - } - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { - if (orientation == VERTICAL_LIST) { - outRect.set(0, 0, 0, divider.getIntrinsicHeight()); - } else { - outRect.set(0, 0, divider.getIntrinsicWidth(), 0); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundLinearLayout.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundLinearLayout.java deleted file mode 100644 index 89bd30f1..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundLinearLayout.java +++ /dev/null @@ -1,148 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.widget; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.NinePatchDrawable; -import android.os.Build; -import androidx.annotation.NonNull; -import android.util.AttributeSet; -import android.widget.LinearLayout; - -import no.nordicsemi.android.nrftoolbox.R; - -public class ForegroundLinearLayout extends LinearLayout { - - private Drawable foregroundSelector; - private Rect rectPadding; - private boolean useBackgroundPadding = false; - - public ForegroundLinearLayout(Context context) { - super(context); - } - - public ForegroundLinearLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ForegroundLinearLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundLinearLayout, - defStyle, 0); - - final Drawable d = a.getDrawable(R.styleable.ForegroundRelativeLayout_foreground); - if (d != null) { - setForeground(d); - } - - a.recycle(); - - if (this.getBackground() instanceof NinePatchDrawable) { - final NinePatchDrawable npd = (NinePatchDrawable) this.getBackground(); - rectPadding = new Rect(); - if (npd.getPadding(rectPadding)) { - useBackgroundPadding = true; - } - } - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - - if (foregroundSelector != null && foregroundSelector.isStateful()) { - foregroundSelector.setState(getDrawableState()); - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - if (foregroundSelector != null) { - if (useBackgroundPadding) { - foregroundSelector.setBounds(rectPadding.left, rectPadding.top, w - rectPadding.right, h - rectPadding.bottom); - } else { - foregroundSelector.setBounds(0, 0, w, h); - } - } - } - - @Override - protected void dispatchDraw(@NonNull Canvas canvas) { - super.dispatchDraw(canvas); - - if (foregroundSelector != null) { - foregroundSelector.draw(canvas); - } - } - - @Override - protected boolean verifyDrawable(Drawable who) { - return super.verifyDrawable(who) || (who == foregroundSelector); - } - - @Override - public void jumpDrawablesToCurrentState() { - super.jumpDrawablesToCurrentState(); - if (foregroundSelector != null) foregroundSelector.jumpToCurrentState(); - } - - public void setForeground(Drawable drawable) { - if (foregroundSelector != drawable) { - if (foregroundSelector != null) { - foregroundSelector.setCallback(null); - unscheduleDrawable(foregroundSelector); - } - - foregroundSelector = drawable; - - if (drawable != null) { - setWillNotDraw(false); - drawable.setCallback(this); - if (drawable.isStateful()) { - drawable.setState(getDrawableState()); - } - } else { - setWillNotDraw(true); - } - requestLayout(); - invalidate(); - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void drawableHotspotChanged(float x, float y) { - super.drawableHotspotChanged(x, y); - if (foregroundSelector != null) { - foregroundSelector.setHotspot(x, y); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundRelativeLayout.java b/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundRelativeLayout.java deleted file mode 100644 index 80510ed2..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/widget/ForegroundRelativeLayout.java +++ /dev/null @@ -1,148 +0,0 @@ -/************************************************************************************************************************************************* - * Copyright (c) 2015, 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.widget; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.NinePatchDrawable; -import android.os.Build; -import androidx.annotation.NonNull; -import android.util.AttributeSet; -import android.widget.RelativeLayout; - -import no.nordicsemi.android.nrftoolbox.R; - -public class ForegroundRelativeLayout extends RelativeLayout { - - private Drawable foregroundSelector; - private Rect rectPadding; - private boolean useBackgroundPadding = false; - - public ForegroundRelativeLayout(Context context) { - super(context); - } - - public ForegroundRelativeLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ForegroundRelativeLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundRelativeLayout, - defStyle, 0); - - final Drawable d = a.getDrawable(R.styleable.ForegroundRelativeLayout_foreground); - if (d != null) { - setForeground(d); - } - - a.recycle(); - - if (this.getBackground() instanceof NinePatchDrawable) { - final NinePatchDrawable npd = (NinePatchDrawable) this.getBackground(); - rectPadding = new Rect(); - if (npd.getPadding(rectPadding)) { - useBackgroundPadding = true; - } - } - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - - if (foregroundSelector != null && foregroundSelector.isStateful()) { - foregroundSelector.setState(getDrawableState()); - } - } - - @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); - - if (foregroundSelector != null) { - if (useBackgroundPadding) { - foregroundSelector.setBounds(rectPadding.left, rectPadding.top, w - rectPadding.right, h - rectPadding.bottom); - } else { - foregroundSelector.setBounds(0, 0, w, h); - } - } - } - - @Override - protected void dispatchDraw(@NonNull Canvas canvas) { - super.dispatchDraw(canvas); - - if (foregroundSelector != null) { - foregroundSelector.draw(canvas); - } - } - - @Override - protected boolean verifyDrawable(Drawable who) { - return super.verifyDrawable(who) || (who == foregroundSelector); - } - - @Override - public void jumpDrawablesToCurrentState() { - super.jumpDrawablesToCurrentState(); - if (foregroundSelector != null) foregroundSelector.jumpToCurrentState(); - } - - public void setForeground(Drawable drawable) { - if (foregroundSelector != drawable) { - if (foregroundSelector != null) { - foregroundSelector.setCallback(null); - unscheduleDrawable(foregroundSelector); - } - - foregroundSelector = drawable; - - if (drawable != null) { - setWillNotDraw(false); - drawable.setCallback(this); - if (drawable.isStateful()) { - drawable.setState(getDrawableState()); - } - } else { - setWillNotDraw(true); - } - requestLayout(); - invalidate(); - } - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - @Override - public void drawableHotspotChanged(float x, float y) { - super.drawableHotspotChanged(x, y); - if (foregroundSelector != null) { - foregroundSelector.setHotspot(x, y); - } - } -} \ No newline at end of file diff --git a/app/src/main/res/animator/click_animator.xml b/app/src/main/res/animator/click_animator.xml deleted file mode 100644 index 7b316a4c..00000000 --- a/app/src/main/res/animator/click_animator.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/button_color.xml b/app/src/main/res/color/button_color.xml deleted file mode 100644 index 2c87a8e1..00000000 --- a/app/src/main/res/color/button_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/menu_text.xml b/app/src/main/res/color/menu_text.xml deleted file mode 100644 index c6d777f2..00000000 --- a/app/src/main/res/color/menu_text.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable-hdpi/battery.png b/app/src/main/res/drawable-hdpi/battery.png deleted file mode 100644 index dd83f9bbaa1b65990f0f0427ff2480e51ac8957c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^-axF(!3HGny!?I_NJ*BsMwA5SrSV zY-i5tcblKO;aZ|-w~<_ZWv|=i+4Adu+C^lZU1nTa_e%Yk->T4SI${+-P24&_1gFd7 z&ugTe~DWM4f=|ZPt diff --git a/app/src/main/res/drawable-hdpi/drawer_shadow.9.png b/app/src/main/res/drawable-hdpi/drawer_shadow.9.png deleted file mode 100644 index 236bff558af07faa3921ba35e2515edf62d04bb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161 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 IM6N<$f~#!pfB*mh diff --git a/app/src/main/res/drawable-hdpi/ic_rssi_1_bar.png b/app/src/main/res/drawable-hdpi/ic_rssi_1_bar.png deleted file mode 100644 index 72b6996ad4d5af7bc00ddb9b96eb14a242ff0131..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1125 zcmV-r1e*JaP)N-}bIxzh+#+XLRwjjcyndTpE;Dg`hGJNAItH>* z#32_O7{F92<)m7z9@S_x&LbAHneW(QsU+LkNhk98{D*3_y2vnApqk%2=qcY9C5P}+s0<-BbwWV z8@He@yFFf|e=Tq?lgTpde>RB0>+?@g0|wASvB-9s2?ghqx~@*x(RP+hrFQwGdQ;H? zG1BXFD^_c-lSFVz#S)V*6t=6C%5Irqf=GFn>c4{&-KAEk`V>lKx3;V6uvXipLWZqL z!S#)|rBZ3Eskk-URh;xKm&$Ufr4G7S)-&+C>;LAC(WTdtfjQi z)(kqA%T+pQS6ehjcOlkx0#+wl|3cT4QS>x~lK>vE7ZPflE5+;8x}jJ83rz}Jf0pv8 zGx$z47LUOgQbDXg!Wxezjthd%7TJo1a90=tZk1Ym4FnQ=;X=dWah=1WyZ}IsU8oIx zXEvMqNZLX!V;Dx3NF;;VTyE4c(7#0NkhE@uHUhW?bCB#f)_qurD$+8jb;(`;>^-p3 zQtWE0-LCcgf@2q4!>Bi%KX%l9UPuk*^92TFLkL|LzgDYkgzI{!)9LI6gI*;tmIV~c z2(=onsW)$x722KcsXX6y5e(gb-&1_2)x0CCZ&t);C?I9qfkQ0K?!BVba5HH58*W{- zy9;ya1r98>Xf;AZ`qI2Eg75kh0T-6&SlWZ8(QmhE8A)J?fNL)fj8flfmO3zB zXsKW&0d5B^B`o#EqtWJGy|yiW`5ek-|NKi1=;$EP)w4SPmi0E;ZtKqJ&HqP+X-hBX{Lf|eOHR_H{r|u3_y4~CcR3Z8N+ppH;+gkZ#A1=Q z)_2GTzsu=Z6~YcN*T4WKlSv2L+S>NZ<#LxnuhY?8OH!05l}g96+3cUiVsSj5FO0gJ z_Hau9d*;1Ti9~XwL!mgO(P~`VicYQD6^0yfNheFMA!1Ly`xmRe1ig3}3IcPn_b znIw|QbvmJ5H)%mKQYn?QX0yq~CgD&dmCtT$tP~4{^;|w5M49(?`EO80celx8Hi=Z) zp=fV^M4@PxA;Hc`!G*Dd0TwQOtAf_La$o~pXy_M!@jfsU*yPH}2AjLdhsV?DOpb0A2UY-9HA_(7RX*=1 z+b0wb-=Y)d&>e1uwAWG(dTnj3&`Mry&={QnTbm`Y|LQPIIacfTPQGiE7xI;~_w!6B;)e z4P8tXjs5VWJC#bE2?hdBCzA;Y8*f>yHd3q8U43)pjc0JyM`Q6QI`HG=u$;XB0MJ-8 z`m4v|u~8Js={zi#Zxgv(uAtsv*w4o6oB@CO*%tvmKx=__@7Z$&*W;{#zaJetF*`GJ znr-MZ6}anuoIHvtNF5d3tJUAKzYWBjF2ShrbyC;BBiTYCa;#gJ-v)WoDuLxpL`zrXLiOyH_IALrr(BN zA9|5#mcJVbKSdY5_wl~pM~06+PgjSG;r;1bT3iUNuGW8rY_~fy+js03!f7Zr@8x}u zeRpmYl{f!zBXE^UB~q)^uksqw7cTx}8NYV*A!vdiP!@G#-K$3O^|$9PO-@X_P@OLT zw(p52yN(=sDa?D)s)mN}wL2VrT-3>Wp?+&}a^gWnz=dU1mL5PG493iRC*M_z1d!A3 zf0&L&mkC^0Rxje=LC_kZry{mGIsb2|)kP+R63DpG<>i4`EJ~n;zT!JnpNl%ZKB3p^ zKYnen|AYVCzlYVJb9e5H&}~1VZL~X_r}zZt4H$-s>FFsO&RU!fYDNP$QvM;y|-$Dz*$57n*NaUV)iH3jxbJFkNV& zUtJP&4PA75rm4gK#^>4}W0MKHwcv)yz ckbeaj0F#H1@EcOqi2wiq07*qoM6N<$f}kjL?EnA( diff --git a/app/src/main/res/drawable-hdpi/ic_rssi_3_bars.png b/app/src/main/res/drawable-hdpi/ic_rssi_3_bars.png deleted file mode 100644 index ae512dbcda218d9d2d4ae63db585a3920a87fdc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1356 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 yNuUm_0QVx|TVLf>Z>deS zl8!^_!$SOsdQ3g4R{B2Q>dQ91kG+gJ(j(Jq8{e-ofjX%d0|gWjs`u4b>WsQGM-IJc z<69V5g$4?!TO0dFQ&&%_d|Kl_3+w@wftwN0>Nn7sdKoyE0PX^t3h_rk1rYe2OQ9Oj zNY~qo?IJ=!~boW(YrQ5dA zHIUj+?*I>gW+BuBrh(h)fh>}$BkDEvw7TS<_E|lzURSF*6xyGr=?1X1O?(S*5!eh& zYD96+-PYP~gb|T?X>}C13cLq)rLpEyB43Dz prLr~~>V@D=;i{cv2fgKI2{1xy8u@QISt8(2@yxQx4Z{Zsps{Xe) zfZ>9Cq}MQ?<4pB;<2)wu27c-l$hvaV*Z2iPxV~TxR7{=4v)u;SkJoXc-0^1VZ^9|O zQ~g=Y_rfysIE)9ep>%ujD-Kuxg|gJvUH}bXEjHm%d|$44zQ*^J<#t@e-fNmCx6?%n z_z{2L)3VNR0e)P$YO2G%2C*G4Rv64;a=Ada;wp?cg8N;Z!!c~`aG$s8FsOuj2rpx7 zxj@%p8}?$ZHavx09jxug&RVm8ryHqbw5&XEF)llf_wW+7b-3=bTC<3!N_QKkE00~U z)-}roT91+Cyh*{#;S9#nSMdj*;J&iXIQqMND*N$%Ba`2b-|;1`F5M*V#cSBuZJ=G1 z7B=9H%1_fZb~lccrHP3r2_EisjkF!AJ%c0F{^&y8)HZ(vw{#O|Pvxvu9rvQgHASCk)lQ5Gom@*_Qu$j9cE+x7gM8F$hGh*C0ZjARDd_R?4%7hUKrvA84Ih`1q1`^y%7D=kd8#bzACHom!vPVVPEFI7}an%fM2Yfck&>rsW09RH@q}um`k#xxWfn sp@@joi%3R9?nGomDfN{51M;uz6}K5zIQvFeYXATM07*qoM6N<$f{2vV%>V!Z diff --git a/app/src/main/res/drawable-hdpi/ic_stat_notify_template.png b/app/src/main/res/drawable-hdpi/ic_stat_notify_template.png deleted file mode 100644 index fc09568bc888db13f79c8e5c2cbb433a32f4cfae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 546 zcmV+-0^R+IP)4e-6vzK>cgR6dLr9Y}sYQfTA}Ov4L=fu`ORos|0Qmw*v?%m~f<4Nuh1H1F)&HFL?X7-IV(@Zn{2Lk4RxoLq}2j2O% z{x3mVzz5)p-@5XBt(yc{26h;(9bma>m;<2a1LhSl16vO`XaZy#xEKOa1$O~#PYMwM zkAYL_)2W{`8W&~_IH5V60BdEjtpMUAeU@}zQrz(V=u7%4>7ki1=qxVXBT3&RWjC;4 zV@VGrz3~h2Cy<$aG_z-to=AGzAWT=%xqnVVgDd@xW?f0$EX(f3as0E^@6_+#i{p4v z(xX;0GG1RQ6Q_RP$jb4d~6y2ro*&;gP$c2nYg77BB%0^j|ZS78nf zRi5WAP1C3_;qeGC$DvP}rcs{fZek!{1IU2qz*5k^0vwfq904oAxFzqK0ULDywS!<^ zfKG}1wQuc$t>b+LYS#@k&?T@4Sjl+vw#yQ~p+M0!b9~;PBZt6d0JACS(A(xK{7q7; z?C5bLo~VD*uA~>hd;k742vjxK4@oa2-SNMweB3R+RYUo))kIyi9~5Y#s0Zxdo)UH{ ki~Om*22Jfnai&|ZU($6IP{#gyO#lD@07*qoM6N<$f-8voc>n+a diff --git a/app/src/main/res/drawable-hdpi/ic_stat_notify_uart.png b/app/src/main/res/drawable-hdpi/ic_stat_notify_uart.png deleted file mode 100644 index a0811938253dfed1dd2c443909cb306415f6e8e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 696 zcmV;p0!RIcP) zOGs5w6o&tG)My~|L?_L{1`Ps1C89}D5QI^Lvmh)QG>K?Z1m(O36cjxM6-YuL1ST0p z*C>J>LP$h`h1tWTAkFmpy61ciTGsYzm%^N#Z*tDR&boX5`?dC!Qc_Y4T(mk~%)j_8%6EO>bKG!|KDqvfX)9rn=0KJnwez`(wlThxH~%*G|%a{W?Lk1zE*Z>y=K$i7K>>^V;)z46>hVt-aidOmGo2eTBp z^Q|P*1S4q&t_OMoXavTBSSyeT^Z;-b7!P9CfX+alilGb`0(Sgiw{Zu!;8WSp91SWAnx_(2mN%%oz#$;#dfas;IE6QW zBf)Pz1BZhHFv%XE6c|XLwm^ZP*w_9NPy*cd2C~2=H+|V-jjlKPQ62y)y`87-u)%Zc zgMoYp$`jZCj3iK2a4vzplj{oX4@T9N!1lb6)&VzzLe&K93chC(*aF-Oa!z~OuYj{a zRM1cii-FN#295*0L4k{BD1iAoH$&0v2byD;5A5^Unu2#(uykupdf->dMWl}=jm`23 zNsyM9R3~XS(y_?w;9nV%q=`6QSR6$S30fJM%KSt!3pW;-Mv>l{bl%L`6R^mP^gm8Y eN=iz~EcgNF=EX@6J!1U;0000R8<0#)^t}M2Og&v3LpZLdo=_BOP!MrA_;(JI z&ARd>LXBA)x?GPxIJf81qW+y5e#gwnSYfek()lO)6XrddYhIzn#by0fRvTylgQu&X J%Q~loCIGUAC13ym diff --git a/app/src/main/res/drawable-hdpi/shadow_r.png b/app/src/main/res/drawable-hdpi/shadow_r.png deleted file mode 100644 index ae07b59379f5fe0924138b29cab52307382a0786..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^AT|>R8<0#)^t}M2I14-?iy0WWg+Z8+Vb&Z8prCAK zNJNQqer|4RUI~!Pz~Eeznpl#WqEMb$lA+-4=^K!um&y(lH}Z6G4B@yQdwe190R;wz zgUr#*hZoPBSm?c6LqdpS`ko&Q`TylqLPSznqPvgt{8|0bh%r8&eUr=-Jp+)jp00i_ I>zopr08LIT;Q#;t diff --git a/app/src/main/res/drawable-v21/ic_feature_bg.xml b/app/src/main/res/drawable-v21/ic_feature_bg.xml deleted file mode 100644 index 70b4c522..00000000 --- a/app/src/main/res/drawable-v21/ic_feature_bg.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable-v21/ic_icon_button_background.xml b/app/src/main/res/drawable-v21/ic_icon_button_background.xml deleted file mode 100644 index 6b1e883b..00000000 --- a/app/src/main/res/drawable-v21/ic_icon_button_background.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable-v21/uart_button_activated.xml b/app/src/main/res/drawable-v21/uart_button_activated.xml deleted file mode 100644 index e00d0b72..00000000 --- a/app/src/main/res/drawable-v21/uart_button_activated.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable-v21/uart_button_background.xml b/app/src/main/res/drawable-v21/uart_button_background.xml deleted file mode 100644 index c2a17690..00000000 --- a/app/src/main/res/drawable-v21/uart_button_background.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/uart_button_normal.xml b/app/src/main/res/drawable-v21/uart_button_normal.xml deleted file mode 100644 index e754af7a..00000000 --- a/app/src/main/res/drawable-v21/uart_button_normal.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/app_drive.png b/app/src/main/res/drawable-xhdpi/app_drive.png deleted file mode 100644 index e64055116744ecb47f713503a8408cfb0d142507..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2573 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 diff --git a/app/src/main/res/drawable-xhdpi/app_google_play.png b/app/src/main/res/drawable-xhdpi/app_google_play.png deleted file mode 100644 index 14e0258a971aeb3bbcd3b55c7db2e515c7f52528..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2247 zcmV;&2srnNP)C^%ciI=lA=~T~?NdMc5Kv%4>C1pcP-C5U^Fm z@P`#6R85*{lSb1tF`CkcF}0FLU218p2$4rsT2NR}<0DcF%Nq(s+NG;l-3ndUrMnBu zD=h5ZIsId1?#x{73YK@$NhUMD`OUp^zUQ3p_xv7kksp%(VAzXq-qmM>e#38P#`~l1 z+;vd_=sWd|$@sdc*HEQTIarbTV)U71H{G@4LIq%w4QrWZGs3=|hQ?2XjPt+e2_H|{ zv~=!80l+xZ#aJ*_sIN8D{lk*6KA!O(`0kb^%@1s?{OJXqLIa|;kKt9VAq7JNT76f^i1pKtf;=o;ACR`S8=u6j{YHf#EqIM)9ncc=#{P zsb92x{@#U?&RGf#f9d@O5iQN-Js?KHYs3o)Bo-Jx;3zi?JQ8q%kPo6rDz(5EJ~6`B zdoR1U{w&L=Ny}*1wqBG<24myCZFMDVtnEUPjnYWT7$~v?k%>}Fbf^>EQ@QVtOWruR zaL`$cz;&;F&>(0mqY_AE+?Jg3MDA4;Zn*p~H}^jj{F!JdvE;6)d%zlg zyj_3%aOO-5=F(D{Xxk_PVH9$FZ~-5RGQ#Hnbfv29QZ8|MUN;F)7Cam9!*h!BxOBkA zJ@3)C^GEvP)ay@+1Ebzr8CT*w6^i%}=YP5#aiz^SjP7%Q+xs==R4C#=k{=?V)=*mA zOusd|!2uRf@vmb7+d(J<~sB0ArW0ZU8L}tWu05#5~td0*U*AXvBTTRTy2` z!ngZ20-CA|K8Bv#n<(8-pENpgYV_|&Tp{xIcDbjY07-^r&v4*OoAC9Kq=Uo`Tch-jVLJIIJ$ERcds@qXUvQ zyR?jo21Dii0+wIIPE4bMh0j0{u;E}2N;)au*;7N$%?*&r^L)P7Q+=s-%5bI{=vokf zjTsi$91W`@VpEDj) zn9TXJwG-1Qc+Yo+Z!P%?w=G&n)>^D62e!3<&C>mwy@E(YAWRZap4Wrtk7MO&DX++O z903QYcqZ0e;6ly`%S*~2VlM(a& ziQ4d&YU1eSrwptSkSTfD#rI) z+QN`ojiFRZs5A1JAQj`+|7iC4++uPkOPed^wlH{BBfb-Cq@Q9Qvgq6i>mO=6-+N>o z0>~otTYQ)+X19c%HwxM3!TsXS4fpTAP_|j~0UVER`oD67K{F1Z83F97cGdAyHcdWu zq3sjr0gwtGX7*Wdl>Wavf_i3!AWz)4Y2rFAmgDD(KAbw|+O_Y@y>ayz{(qeP0fcGsj$Xd5fGmJ)rjR;2Yo z!KXG56$`Zw7L)`L@*oz$7vDv&pAHK-QBsnGs*0q*+kg`1H{mIp6c=cVJKUWKaI5#SB{f#*fY)5z#ZC zS*P8mum#gIvz2#5;3wai;jW`8A^-|N2!i(k=Nut;LI~t}K?s4ODCl-`j4@g5bRis`~JIFMRdQVE~dri$Ua>v+rHGE4M<(7g66K*%z(o zSiYlNZtbHeK(+TkRdLR*D(k(+d(Xnc0{_170nKKUEUQziCCtqo;^^(4qE@R>6kR&Y zhQGhxLRC>UKuj75lWFHq_Z)xrn{Pb-=&wfxpsFUfLSY4Q0$vDK)`0{Gx~e>|s<8LI z^gzHl&r+++&t9HmY54$29AgaJc4&gfkJZ7%G%vIfY2duBJ0rzt9Nl@~gXd12I(hHY zPtU(O5`b$1@u;h6!W`>=o8c^9002?=vGDt%RrQ(h} z&#~5F3}jhGmSxnl+Cavghv4aHJbRmz;N+p5IL zsMa22ogVKCobzJXWT#zaOf%Se)V)$+ZfW_kFFfgl0HYAxe0Z+SxSEL@6dE{2_s zfgWw*IOD>Ca^b`OY1s8J}5fGID+(t zhlZ(YlTczE0J5~YB0V~;OP8iEBlSN@>NOgZGv&v! zH2^CG+;$fE;nycQ{*}*Og`x(KVb_aINt(d3ziaXH=g%WP4CdZuJfI?c;?{k9?w-TD zcFVf;{f{taVTTHtDl2JPT5Yr_#-p_=(e!>ynwG7@@cOfV>NAoPOjXrXP9eh8k=|Lv9+^KL%DZ z&^5BqKmdW!^YHbS!IL@Xk+OH`SO23YpQ3%{)$tg-D7fjTPf-8N-J43I$>3>&U)!M1 zg|m;HX7;huyIcphKp_{xx=F!}D+2*&5X=ps`xbj4AAGe(U#SDXd6E3=JG;{hjcC1O z*u189Eew_QsS^({b>e|tGQMEcZ>^aNL5SUlOy+;YazCNdq4~FPyyl zYk&CFvDx3Bu0^fm(Z>tUhohqo%eEy3X{g(+kc))tqTllGy-#sj9 u=7H=w=hM^}^W*vX`PbN!J=v50FZnNPG{gXUri!Nk0000{MK~REfLv$`X7Rp6u+_e_^WVg>6Nvmdv+JcrtRN!XaC;#HM{z6u6mJlYlGIa z*M_A#W|+nJNb4=*6~Elpoq71^mc*ztPpXYBE2jxvPMW&!MIIN336lgnCY5D%Qkwgj zbu->r_e&`L(Yt-BraaJmlVEq{(abdtcH*rkkGQ>T$(bb}os)5ti_@% diff --git a/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png b/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png deleted file mode 100644 index fabe9d96563785c7d6b008bb3d8da25e816c343c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^{6Or)!3HEd1bTh|DVAa<&kznEsNqQI0P;BtJR*x3 z7`Qt@n9=;?>9s(?08bak5Rc<;uP@|nU=UzFz%6x@#ly{E6Z5RU$2Hhw*i>Gs`(~~C zr~2_>(e4Ka`gpa)&de}Ks*u{@U5E@m-v9X3<$3NT3r3p*`<7|@n1Ecw;OXk;vd$@? F2>^8yH@N@+ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_bluetooth.png b/app/src/main/res/drawable-xhdpi/ic_action_bluetooth.png deleted file mode 100644 index 920f5cae72fd854fe4fb4ddd628432d452b732a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 344 zcmV-e0jK_nP)mw=~eoFln_de7rM3KbJV%29x(;Ns%pb9sgNo>)wCwP7#=^{=6abF*ACdK{diJ zZ}!Z2l05ST;h!&nDKhU*6nS5wn)f4AeoHlUOD8=6WsEVfCra+wgx?U5%%@jP07Bz2m|lGet3Q7A6s|OWuK0%`5)QXyaAp> qB7um!0%GzBcovBS;_}xKG(G|4td3Rk)`IT<0000DtHLV_oH@JL830HF`mTKEtvum~D3@jwL>;EAI(;E9Cf@!&Gf?9Q(2>^lde zbAlK0{IZ`T{jXoO*t6#$CjfBDNI+%+9C6<*G-uohbR&2I4lx1@2-Za)LO_YQO938T zph9@*JcOIiG7;yY6c95rf|oOpiJ<>Q6yMhFA16kpgd{QM;C`PcYO zcs0uZKv?7(h?GaH4Pm^p6HGZ~B$)CMysB-9O5T=W63N>UwRsyN$gkWdt2>H`_s&@b z8;%7L2U7!cf+^C2Md+<^%43pe$T;F|a3%N@@L*C5QdCD_Kd6F2qqZwUkgcGbKG zeCIdxyxX@WQQ1x^588&FB(Zdp?d7MbADKn-9bV; diff --git a/app/src/main/res/drawable-xhdpi/ic_help.png b/app/src/main/res/drawable-xhdpi/ic_help.png deleted file mode 100644 index 0e67d7c12130fd86e9c7191c236b31aebdfe890e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1796 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m{l@EB1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxKsVXI%uvD1M9IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr4|esh**NZ(?$0 z9!LbN!`Ii!Gq1QLF)umQ)5TT^Xog;9W{Q=GfuXaxfrYE7lYxt&p{t>#iJ_&diIcOV zo2j9>iIX8ruSMv>2~2MaLa!T6y`aR9TL84#CABECEH%ZgC_h&L>}jh^+-`Bk zX&zK>3U0T!;MA)Rbc{YIYLTKECIn1BASOKF0y*%cpPC0u??u3b{ao69Eik{7dAc}; zRNQ(qJGP%YQ0AEb?cCi{mTKQf5L7z2sUy#2NAdGhtsNcPyT3BlaF*LOa&xW|i0Kp% za%9>2X;RLHkLQjpGP!Z-ZQt|xc4y7bp4rm-*5~|7%ai)I-)=Fko@e?0y0LVjjC<*) zj#Yf}VGP<0q6fGVST`_5Fj|$Doc+RSJhSKiLrVqLOEs}`+x{`!ca;0{)ymOqc5Xgn z&AQDJYgqp#*m+#3S;k=3JUxNs&y^z9D?L*a1acT_75tPEP92qbx>xB3>$_&_2aEHV zPCU8RESsP_oUIM9el~DAzFbwsdQRp%k}kpzDcQa?P5Q3&9TA%f#%%Uw@W4;S!vX- z)UWz#O-pEk1G~I}T7hiNTA>|}_~RO$Crr5)8|I}S!CTh&G~v=={SJ@U2b5(GJ&->3 zNm0$5{~)v9zy8x*f^yA|dFJMIPT<(=vfSo@)Q@9V4Xzz{uFAZJv+qNmwchFl;p&WT z9>$MteV3j-%(Kf^zipD8pyR)5g_l_)f<;yDIHli9VSLtxiKw{_)l z)Bb6yg&gD9Ajz|Nhvtu#BZYo-A=mpR+?IIoB>2?s@9LcAvkPX~Pt|1dP%hbj{)x8e z;u5>_+p=!9Yc_<`{MBQfxJs*YeN(sb1)of7#-J^=>)AZ#Oy2)sB4?ImTBKfMQ**L}QKp|m;ME3xkCI&sj>`{C`oOd4pZ{gWeW~x}o5wO# zYCM{u_5PiUCwvXlgMNediCw&JO51*zs_Dz->=WY9>Q&AcgwZGgnMCA z(fyATzb{qXbufhSSB36=Eq|ZH#+6J*q|OL0TE=B?&7oJH&oX@#!zS-PeMcvRSNvHu z>%PxbH-?Sxi&x#bKi#ij`>eXC6tVvJAl*6@y*D-vLh~kP{qmg9^~2K4d0wfL+7H*M z@=Xz^QuXT`tNzO$@@iH7w*Od;X$CmKuVDbE{{_y+?HSn^+&p)g-|%3a462trUHx3v IIVCg!0CTakTmS$7 diff --git a/app/src/main/res/drawable-xhdpi/ic_nrf_connect_feature_fg.png b/app/src/main/res/drawable-xhdpi/ic_nrf_connect_feature_fg.png deleted file mode 100644 index e13cf3b6bc7d4bea04e148603d775e3ce56f2e86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 846 zcmV-U1F`&xP)Fa=kwYSE%L z6_gdNbWvojCD80^A!%5C{Mt-#JDqdix7Vw^!F^Wup7YE(Gjr#jnK@F13>h+H$nYN{ zTFcFVO_Hi4HAt$J^jXq-NnfKV`dYMJ0wUlH@BoukyaCikY$G=Emy*^Vqj^Wi(ei!ieFJk&T(36KlrX9Ht zKzZ8AZv>uD7zT2z?fw%pKueNLNdY#Tmvq20z9i|XqzjU|CB3q-IL-p?A;(Bs?+lkp z+LEUC8VXro+WoX@7E+% zdU*Y5dH-l2bjD(wUK@J{oQ#dv10#;yP;9;_u=Zq{u|FBuq$$Q3a^^;W`q=ylGY+6P zHa-?u`$$^KpAKwr)8P$xI42y=dJD5X_TCYwQlE!1b|D75_G~Z%YbOyrOmD6r6b9CFM`3@EpfHjZha zDu-N;mC*U(EwF2~YX4zRr_`5_v41@`Tj=D%?OYC7sR1>fP6=xR8>}s?vo;ZH8!Kb- zZNNiIhM%nMoC59vd)KP(?MU$8gf-}KjGH0xFH&X=f5X4Jap)}I#~duSJDy_}0?RoR zU$D!pb;WqoR&9ysndhXLx9h_*MSr;qR7k3jRA)8+7fF+nW}+zimff3Y$dDmJh75V& YCm_tD{U9=O3IG5A07*qoM6N<$g0X#q?EnA( diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_notify_cgms.png b/app/src/main/res/drawable-xhdpi/ic_stat_notify_cgms.png deleted file mode 100644 index 4ff0268acfa2f19920e0b80407548eaf086e947f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 844 zcmV-S1GD^zP))C|U-I!61P&8f+{C6)a4VMo}<|g(nda zv9Jjedn1Tw1VKbZ5P$IGQixte3k~>7OprV_b1*Kudpolm_X7Jice}Uq&inn$%=g`j zWXO;q!;Awk2bj?YT$o1gMj!_?8TS&MU@r+WmZ^XPz-Ru&PoOsuBaDf@Xz~*YiXSE% zZ-7}z7}4(`Zi-6^3=$2`VtESK;X_)C6JDSPJ9JskrN!3+e*#D^JBn|`9*_ z%rkbH^5+P!(70X-d<5D&bF9-jKIP%PmsGKJBt-lZz1@1pJ^;4*@%iv8U@q|9;C(k8 zrF1uN5x54N0&)Su@@ATZ@GSx!0&5Lp?gEqApv0SvD9>~O1IBfGPzMKeXm1}h?tL;1 z`k!5z0PBH*alKs&90(b=!1(?c=uD%H9X&xdTWQ@}4^g?@I4Hfom^KVO&D^=XOk zwm7NVe8!IftJ2zWHab+4y#~i~Kcj4+Vi++dB8`qU{kVvflu~_ZZJF)F!%$LsYOF)1 zl!(ag7;_esQhSoh`x?h_iyg;(6Om0o*%7H3FzPt2MR#P9S_M2KftQ WBxC=td^AA-0000UAo diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_notify_csc.png b/app/src/main/res/drawable-xhdpi/ic_stat_notify_csc.png deleted file mode 100644 index d0f34662a9b4fee54e1a5e8bf430c00e097c20bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1196 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 diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_notify_template.png b/app/src/main/res/drawable-xhdpi/ic_stat_notify_template.png deleted file mode 100644 index 8b4925d3f68b5ec865dd08fea28ae01ec1bdc031..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 658 zcmV;D0&V??P)VC=Y1mOVPcm=p(Fs^{geFF6pOM(layr7TW;uNYa_4hZ*`( z-$+_Dv!AUOqn78rq?wFuH6=-xz~i0<%%84CaIB-@VcZmGb{PNe;sFl z*KYzlT2vJfP4r_v^NCtHT&5g_9ga+@A){dI_)_uf!nsk zw?H`}ez)x~l!}2S0k9QO2(AEk&bgI{`L@FG?WAg;$!}mW<~)mGd=#+_cq4qr%VDi@ z5p^gD4?`D{F2net@e=r0ElIioPTItugcaq|D3cWisLkW@1ZYUANm}nSS8JZ{l3s;H zoG<~z&j1RjKwGj!grPh_`U}Sm7zR+@38eAJdJ*MzFu#wdkb8zl>yY7-erq={705QWpOzl$&^p&){GB}lu(L}+8XHo8d*tSCx~ zDCkjb3I|K1T9m07f}>2CaW zNF)-8L?V&s7uK(Z51yW?OtxB53=CagBXqIkmqycWTzpw;P7X*~3A_lb1#SYa=<_B= zpaF~kyUlEGPX{Co19k5n1Kt4+Mll+U381||rzW5x7%;dMcpSLb$KEotCwe*X6>vAO z$IPB~*^|Iyz#wpg@5n-g_RoTPFxw6grAG}u0BFTntCmWWbP1m^Nor-f*8AHulc?F# zOE>EwZ+DR$%Gq~Rl48{A?&l`$m!w?($J|7X-ru zk=I>!u*)ti^|3=P3Z1z_A@KGCWdC?Lo*VvhSv5?#-n%(@J=gxENBfK18hZgQFWXt* zKu%{5fSH{H&gAV1W`B5dHE`U_x`%NVwZ=W%;vR1=Y{pZPh9#XV?9hf7^@yZqPJAv# zJs72pg*iqg-6!d}!W<_g4MpkYZXqQ7ZQIU&&sox0NuvNUn|YjlV5{4;&Ex-pi#hO` z|7C03^B?+YeHOLK?f6NOCfcz)Excwrz?(j>(PMUZAB)&{DcErq0uYXvQ#+%J*(bmwT4DRypGcMXyQ2BU~S>`%#=j8hUYE^6DRnvJN zpkmfgE~NtRd72ypzA3EndCBZlCfxz75bPVMR-3cGj7TR7MJ=2DZ|UJEt@m*2a9+~= zQF`(j!0S$Y!VC_EcaXE|3v_f03Vv!j=E&Sa0<9aFci$bp7VnyN_X0Y zdw8eR*9FH6CN9CPR6tp9oxyFuOF&KVYB1aUpI>CHEL&C%;RWEXD6R^|(q2FRyk9PY wU}S3`Gy5z(oD+#eB9TZW5{XN~UjzP(#Lptu?EnA(07*qoM6N<$f@m+jNdN!< diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_1.png b/app/src/main/res/drawable-xhdpi/ic_uart_1.png deleted file mode 100644 index d737be597c4f0fe9cdf469ea9b7b5ec18ba8ebe3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 493 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGovY8S|xv6<2KrRD=b5Uwy zNotBhd1gt5g1e`0K#E=}I|Bn_x2KC^NX4zUH#T}PJBqkH{4?ED#e3Bcu4zfC+ms@r zZmhbo#JD#@v1}SwRKyPv!?pSex?xTe8rCjVn5~dlb}UNT@laa2#eLy7cP7t#le3r+ zY#Ed|GdCzYAaa_m@$3H)OSZ&af4=od+y3v>KT6X%dF#V3W#*ipt7`Oz+4K0MMb<%! z?=e39SaGttJ~2&w(xMpeE!GJ}GfQs2{noo=>x_DrK(>N&xifF>T=Mir|M?Y6BCZ$D ze*WWgsZ2|sKXA_M=7R@LZMt*v^B=pV58XF1&ncdrR;H4vdUSR{*yDrwGa} zQ)u~MvfcHQCX*){hhhbjPTmjwb32`xLhOI|zyJHRz~YVR?6cwbnpVmFJ6vM5_ic%l z{GPTj>mTOt|AtOWJ^zO@-1>+9`@eTHOm6uX{@8ktQL*Lt&788kSzGVz?6_Jz{ZD1$ zV?Vdyjn=NZixa&t;uc GLK6U2#>1%q diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_1_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_1_small.png deleted file mode 100644 index cf8880f83b4af0af646dd7f9ff5acddfef28340e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 355 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVAS$-aSW-r_4b;f-(drZwukSx z$0#a_vPB3mi+gBU25!68w30bVC+Q7i75BOZ3r1%1gbgBwB@dW<)@ZmYr9R>~GVPCD zwA$_bDSxH`4FwvWsk8j7iSK5U&9nDEmw5bZ|MilcGIlKmscEZMUO%hkaJ^Lh+|I2k z@i7gnRy})ck*6|uX~g~IHh+>f&ZvHGK1na=tzu-uvP?bIyR&aro|^tZSazT3=4em% zmG%ccnXi=A+RQ$??rhG?(_d!lIB+P=xOwV~U-7PLAF%_L<DD$~A^-pY diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_2.png b/app/src/main/res/drawable-xhdpi/ic_uart_2.png deleted file mode 100644 index db2432c1bb3ab05b9925e4ca6fcd04ffa3bcfb69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1278 zcmVOXWsn0nNX7~4jJ?+cPd+$5*d-G;PB!mz` z2qAkDOUox4hstlPJe&@lK_SR?AprTGINcHW`ZF2VtRV|{P6H_AOUQF@B90e zQey!6uJ>5XJm>rVX~%JX58F6Z*g7;jJ8K6)aKy5#_qJp5iHHE~wQc*;AP5d;G8xDn zaFvoXFfcFyV1J{|!OUMxO--E$OGye%0NHF-4Gj&w)5yu+6v8J;srMy-O@bg80r28Z zoQs)RYdsQ+#V*JiKt#1#?N%asVaKjI0G4HqhGFb>C^})=M*t{;D_0ZIxgZG6dY(7C zxVX65)zxJs5{ZQ4ID3>*`-tcOfSU|LNPC{QHx`S{wR`}~{5cWb4#2ren_oT8JK5IO zws6^39uZZEXqkx47mLM@5{X0xzytNpYgyJH5zV!H39>BfORe=`W-k8E4kH-t8Qt2Qw|F|*vf4i>x=HTGqmY>?`h4Dw?ZRoWq7K@$9WOAm?;=ihtdPMGk254_@-)+$BPboo-d$LL!G|Ni> zjoa$hUehJr<0j#a9^#FLzVDbGhEPZaO0OIlZD*)Q-T>a;5 z+x}Ss*k-=(4+40!&g2Ka@4p+Sem6w`uIr|hQg0YEopK!Kk_52LIyyRz87#cZ#bWV; zu=PFi0cfos2k@Z5!aLF3-CdCYwo$cO-38!iy>`re#1~4--qa4RMW$@Iq zQZAP#!#a-0{vM#UKB$y>$)Md)+qP%II*v%*0iNgG2H=oE>G?X$lOM?d3WY+OWm%`{ zy=-IVl}e>D8TRo+E`Z+N-cbO144!)4>gwwHM*`U1-R?dB!v>}28{4+Oi^7;96M*Zw zNu|_rgUMg1R4Nlu8CPTi*x2pfY*2dMjzZ~)Q~+A*#{fKJSbDyVQdtds0M%->55SmV z>6xU633UM3Y*smrbF$uUcT?#pMlFC)?}WA1&nu-~Hn{rFDy3eJ;-)gRcR;OHy9L0A z!Q`(->8U3Y0Tc>_HruvO8~ketG4n(eOHZgvP!I%T0G`_M$pG$d`8&Y({dd&V*q-ng@h&mt@Qx_4^xW?O#lF1r4|&X06HW9A%qY@2qAn+a07*qoM6N<$f>V%N!~g&Q diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_2_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_2_small.png deleted file mode 100644 index 855a88ccbcec585e1bde07b5c565a39ef9e0660f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 903 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 Y3!;AGrga1Q@&Et;07*qoM6N<$f<+ckpa1{> diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_4_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_4_small.png deleted file mode 100644 index 68b32fd90ebc31d2c8bcb21f7155460d2fec39f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 559 zcmV+~0?_@5P)FkVm@6+vU@#cMI-u&1F1VIo4K@bE%5ClP(HYn7N8DqWx z*m`(Q)AZ0q92H{nTI(fd{#i$?7Xcv-vWLqGJHB>xvnafJURS z3*dd7F@q@qW6U}LQ+HeyNb)NHe)x#(T|kl~i_E;|YfJVjpwsERveq6GQNvdsyb0*_ zddgaRKtykS4uUrUgTY`Ez$f1&=tV#r$7=w-R@<5RV&Vm8tv@QIw*K?#GRv~_y8Bb> zIiS^Qy-`XXRpT=A^=LHu1Ar%1K)2hSDa-PRh~_GjN|wlfYs_oz|6n;{rthFb3p5IRNHkc&I zJ7(U0e9UF$djO|-p5KIU4{;_yYd!n#m^=Hp>;e2v)ATx&Tc|4mq?FnxqJ_$$cb;Y0 zRY>0;oZP_L?e+!{eRtXMr}{{$93=nQb@HK?zz&`fPcDFnW~N%eb7SfS1ovyq7<1-w x)t4C{0z`la5CI}U1c(4Z5ClOG1VIp{o4=&Rf);HQfI|QP002ovPDHLkV1gSp0onin diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_5.png b/app/src/main/res/drawable-xhdpi/ic_uart_5.png deleted file mode 100644 index 190332e1384b0a08694159bba6e10674115bf294..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bE8P)Nkl`A$Uy|n_RvEq#9*-C#e-Nx@Zdl2P!YvL@y|h#B3dK_lpIPe zJ$MxCRgi+GDm{2o>s~?$v=K$wC}JA4li8)aGxMH@F1WOznMh{mCwxEW*_r*l@67Kv zZ)TSW00000000000000000000VBfKCU&zc_BO5&PV%6gnoP!7e0RRF(0Du4x0F0jd zUp6lu+`r=rKmdRM5C9+m1ONyCeZXk-`~8E?xno4MKt%Tw(bQlpH;L#kBKn<(uBvKl zV`Jloxw*N%w>^z-UueHy$e3A7L>D@p&S&*{{RRRUaR9?XnAu7Y1YcndsNzgxZivW7 zs`?rNs5XdK@;rYL0aP2p>uH)EK>*bS;DR9du+?e>2%x$U?ps(`cnp2O-tJ_&M0812 zzs|Dkmv+0oIXgQ$Ha0d^cg`IWk%x%rDQ1oehH#vSD!nW6?lx3aFEaDYZG|@?`Z7t9 zvyDb0zT4-wwzejwr>Ea#<|hj_bx+r7wKk@pfh0+uH?xaF#qX%<8zQp2Tk(mAW@cu( zBJ#oTKJGdNI5&?qU`V6Uh(+Z6JkL*>*|jXoUUklWzt{c5alA5^vUZz><}n48H=Vu5 z%+Ktyu3Qn3mvIGD_gG^JQ&35=EW27@vpNDONw3%YyU4PG07|mHzTPRaR3LzoG@H#@ zkyZc!lw@LJqDV)40|AsI48yqsn_Wi$Wi>g>hYM`BfdI-2;8=lP;YS2eP7iRDnGY4{ zmRAwL&INo#RX<77^zdFkyS=@AKtxUzIQx^;)zviwaGO@E6^O{&%>1x(?!2m=$+GO| z($dnd&#wCY{=vz~$xoU2-U5fZw79sKS2|7l7(kxqFNnxHJDjn<5z#lQdO6FopEox* zuaA$9yC{kdgkgAunI9#h$C+6QUS%(ZVYr3>hQx8)h@$8MGne{^z|6iCkvFRzuXj^W z6h$XSQ2c(Hrl)-vuU7%|dc7tQJzMs1%KA7AXGw+6RrQ>Rob#$#^lc3o{Hoyu5k1Gu_v};gujP5Z5{BUw-^T0RB0Tt2!0}ZX4d|XMOQPsoagz;<>lq$J{I5GK46E%`2pwLJTpH?L`}N&9(Rgvy-v*N-#=#j zK}0{9*;=>T{m#e#0{{R3000000000000000004}@KhOtRhm+;_1ONa407*qoM6N<$ Ef-m^V?*IS* diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_5_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_5_small.png deleted file mode 100644 index 192fde118da5a8b7f0801a52027d0d89248c7def..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 735 zcmV<50wDc~P)5@H8?a&of2 zTrTg%MSwQ|rkc+lfJga!{`u0Hu~Dyf(&M2XL*G1|l-n?AsPnQLesP zt-1pP1D{D=0PxUpoaIWTB7b;iV`ymT5`c3py7<*vA}l5+sghMlV%$GIPy9dLAXG=IGK zrhf|n<}Aya6A=^&1^B)X^6&nG@B2?`60)z#IlINb-_B-z!bvDWkX z{NpGl8KnTP0DN!5pMRFi5<8Rz5{J$4CV@ z`JIW0i4l?)MP!&{zld}<3!4UjLjXSkd;zdY^1b6Y-=HXpq9}@@D2fshzX3AA@@6ef R>30AC002ovPDHLkV1n7+M4kWu diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_6.png b/app/src/main/res/drawable-xhdpi/ic_uart_6.png deleted file mode 100644 index 7b8879c30cca4df7bd202e4b653aa17c3f72df39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1516 zcmZ`(eK^wz0RGLg35%VghTBZX+JP!Hm2p(`|4wRsJ=nXTZQo1+-#rR9(*v_Nw`*(nraz$IBJ(d_-1N) z7*25^%tz#?e${ApU(%>1E0mwhrj-?`!}fHH&gK8)4CHLvr1wC|fr5D*WQVhpliVn5 zE+^9yZBgSvpxtI3%gzSp@p~AfH6!NO>%O>wlyio`AE$exV|zOZ*y>5OMk11%6h4jd ztW4UAZLl#BiLDtSQ{?W%UV&mCaZ!qF>{)RaX9n1)1g!Py$GERzvG@sb@k#3TZ|AY= zlEh0N${^Qf6OQ|JTm{qedP~50(t+}<(MJ!IKWI;lKbx+)dBF(Ds8+{K2%=XsU%J}( zzf>?Gf<#`3U)r$O#*O#1^*Gl;1Avs_wQ)b3cer^h2}2&it9MbBB*e-U@9D@Sd~|fB z?iFtyMjn;7mQ@@RQP<(r6cr5xsn*yf7sD=JLc5*lz#>ODw}2u|toi}z{ks*0k~6#l zWju6Uqp|0zMBrT!I4m}6v0H@iZHS9J-Fj9iy6iNu@Y8DlsWx(M5daHu+AcP(`EVd8`dqSsg| zgp`Q1^|x&cJnq8(m~q2=1kW{H&i4xDQh7Mmi*YMg^1ejC*?>{FHUEj8ZCrbOef=9u z!o6AqriRv5DijJY+Qv;NKeNn|246Kp*ladb9Jwcz*V$PUxJO0zk|iHC9T^`dYSBZP z4}F?mC!{+lhCE}lCNbKp(hEo!C3F_(xV zai@AI*sHNlOjf7upMD${WCtBQi6}y^u4Q(GvWFYocli+7k&Q3$&07I=5je!~<3u|% z#f%`_9TR(EbKj!}Do|Dc+UH3pdHg^SA~imGo#ZGh1V4Rze2eLmpPye@%Jt%xU#2Kk zP2x;yMPDrT0+%RnAKg4VuF5*kl-lyVG5#PlxdQ?K%z)ZLW_D*T``f4VgIRaSztB|f zWM+VsOeSkJLAsx5QV-02Q1_g$>kq3`oY`MhR~HLIcEu+pq2Y9msQmVR!>>-rjywFX ztvufFj!P?I)<+{FBh@A*3{&*54lMR^1SRn8**2r=zPpvRSs6>yA>_uL=hD*nHk>gm z*%YwLKkY;G27=(gejuQmuTT^cXsf+f^kSvllkfib8Zob6*^u||F5Zvda*DcH3^;Nq KkkH^wy8I8Kw9?A} diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_6_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_6_small.png deleted file mode 100644 index 1aa1c473de0926bc6711d77f85da3d96ce7d3911..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1021 zcmVMweR+W0}zfhG(sOqK1 z_R<5Q>ZuYWia1oNh`pPJSdEGxG-+HxFo@W7%*6A0NjX&Z*n;fZ3-dYd+p*_;=goU> zMhjRh7K_DVu~;k?i^cNNLA^2$*LBlG^bUYGl~NOPb90Yg3IVd&>{|d%6Vaako-y-M zwOajhVPT>E!U`azOfd6@MD$)WJ2TG>4-bDcJw2_%oQKm5=Uvx*1;A%SG~RM$h~B8z z>)T4D(#}Dq#b_AR<*#0OK3RB%~#X}PylgaESqJ6CvpNM|wg;Q8)TR;eL%wR4u^Y&~u zd!C360NBaQ8vxb-+$W;nwbrw~@Bbak!%#Y%9vB=P{ECPYhUC{ydaTU7{g!SlS4xKx4B z5Pmbs%qQbg1-7?|5Cvcgz)W9X-#sBjLn*Zjz%c;t8pa)#QvS4+onu%6m~NUjHV;0x z|Mz?e!1u1}E(;;fHr)qALTfz^V6xY@z!QUQs)_SY@qK@qnQs~r?7^PT0YtQJu;n`# zf8Ai-6^A5PGpt}I-Q68S9@rKK0m`imjYi{1TlH$Ss=+)MhdyB0AV9L+`2PO>euKFh zha9lfvOG9W+a}3zoRq;_iGu*QHV-q+qT@&h5oCJj&z=`xW@hFgfQ1&~24~OTHO@kSt4#(YdsMJK{=F)g^2Jx zZ-kjY0N`~vq|VHf>+9=37mLNvz7&c43z1AF^P1y0hXACR`E>w0h$zX-iip+#{Kd@k rT5CTDf(k4ai^XEGSS%KcB?SHjMp=4wJV``#00000NkvXXu0mjfjMCrA diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_7.png b/app/src/main/res/drawable-xhdpi/ic_uart_7.png deleted file mode 100644 index 3e12930e3a5e63fc1a88b4cf28ad74e4d94fa68e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 901 zcmV;01A6?4P)=C@lWBMx!x< zZD{iCKS@N#6asi&EEZb`Af5JaRVtOYIEW?R{?kNsR3U(8<#M@$0McoH&2`xrW1 zA?N&qR-1+_1R#XCDO>zFj_a=LF6%aTx)r2UUL=kvPF-}pPgw(UvT z;>U5^sMTtVTAo`>UVP8<3XHL<3NPQ8k&%%Q0VJ%iuWy2g_DL4M>3N>3?aAHL#jjSY zIU>5QFw`9w82E(%5;in6bb*KtNZKFxzW-Y1>o7I|BAQefw$R(#`yBx!tle&(Afi!8 z`=c-nU+I1=Mh0M6mP-5AY}?*K010cgS|!HV8QHi_r!#FtnM?~HpU+PaktKQjY~*se z&j=u4^?E(e7`vo!@R~K|acWWkgM)+TiDTTf zv<)B#f-zIs?;00000000000000000000 b0Pwfpif=PT!zF?S00000NkvXXu0mjf$JP)M z9G=-+0VMg_Q2w7TYw;^9AP9mMfV(M+kCP-hu(}PlS}hO2D?@m1Fc_R$EnqgA?T}ng z3H_c-CLOCULbY0L4ZsV--rpOK$BESfDwWD}0BeTqA6tDD4#V&|fE`2lV;0$8DFIsR zT>w|oPJYkxjz_1z+$o31>mOL`druTWI1->0ve6RO_Gld z+oIF!_5N5bfaFU<_7jo^S)-1l!R$^O?UisG#2P-q4G>-%X*&?NWsm_uO#eBZyO zwcavhf1s55X|({yah{T_7-IW*&7+VrpkA+60BjqwKUPY8vs!@Xd7A*P8p4OUR$>vI zPiy_mko`E0V`a5~AP63kyln^{jYgxyY5^plnBIpj>s$ZIo&mn^-vMynQ2wV;6#dG* z8|B>kwwF{!2L+IufG`X_l8;P>U$t62v04Ct5b2-00HD`yw-;6m&{}UUiG3;TMw-*% zcZ=jb!{UGR`~B}$3ve7~1Lpsb3I&qa7T7Qa=JR=>0t7)21VIo4K`1AG0m_KJRjDjR Q$N&HU07*qoM6N<$g1%}T`2YX_ diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_8.png b/app/src/main/res/drawable-xhdpi/ic_uart_8.png deleted file mode 100644 index 80d2911bac8d4367bbfe2ea109d7a463e7dd5ed6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1646 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 diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_about_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_about_small.png deleted file mode 100644 index 3be3152704e316c2bd85e2c33e3f6d11398c5d59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 860 zcmV-i1Ec(jP)dx?y z?%nsk_wL=DlSCj82m}IwKp^NP(inq`$K!!|j8q%qGsC8%#P!osA zPE;$HT~w;A6%o5vMF39x0=?}6K)^Bt5orLa&!6#lBXM{>kl)TQEJlX#Xs}Z4&Sz+( z9w!g+eQ7ZM5v5!y!}k__71#y_5^s*}D+&Q9`AcFfO#B|a#`PE~YNp!M(D#jxkVrjE z{>l)u3uR#KiC|DwX~-$pt|X)$WdDVD2c*6Tlw3=aUqA`pUBcnSG2wyd-)pZSLEVO0 zvJQvCHqY}@tS12PRQsUbRqD52oIc+;*fS>y4?MrLRS>G^XrQ_(?G@PYDQXB9;vTBD zYfJTf)d+-K@y#Y#sy?nrPGIE&t^zJd9NNZwLDZ8ms>KD4ZL%pG0qA|5`Fmg4-ZB(w zTxJ`L(6-!xQ2o_fQXSfalZyg^?a`Ur}hDF9algCtwhZ06p29b%=l=H#xfykadWFEEWOm z^)@ir^@%M3|13AQhZ!M&He@X}230_k@Nay4q9CR5HW&eTVxw<93fv*Sl=laO0PD>e zXBk|R!o_#NVwc z|DeYk(NH~|X$vFQ21c#};?LjiINalWNbP3I^Z#i^8qKr5-&3pH-RB0h5e@ven15ba za=pm*W9<^ykNA63ud+gh@=hTkN+w1mE|1K`6 zTEEcH#B={;iBnIw6~c`4)>jyR^yPECV3`+nGT+C z|HkLq>3JV7?yM=3&hmd-kd`a^Y`@1khPAf$kLg^QckAJqS*%^Auf(prnZM1?NG{b= z{`adAJ-%q^^G5f-tMDhzy%8mxz4+yXWwt8yuV&N;U%r+8Y47@mDeKn6gk=dDoJh+r z-N{)!S)sgfa(>jwrHA`vOn)vH_^ipq9{ppF2kLNDYbh=u4|LW%zMLzo?8}ki639V&Vd-(myjh}b? z{IGkWq(S|A)i7g$y$pudOJd%6E)sW`oczyyL+Kr{Z+p?6&n>h`#lJ`@+);SJqFieKXOQ(P~TY z;)vW_j>l8D7Iao#(LLt&a>AeFtdstEtR*!i6LOYMKFgJ{wk558z3rb}R#%^>?`F~q zYkvLs@wNy3+u7qAq^*}Mv6}madx7hWto8M8FXa9!{V~-!=BCJjO}@VC&dP)^t}F34 zS!Q*!)Nz{ZrG;O=DIR~4e%|K9<@QRnL;>XP{VABh5FYF8{HIM?A0+MR>gTe~DWM4f DO)o|_ diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_down_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_down_small.png deleted file mode 100644 index 76937f57abea8b0208ae4af1e9a6a58da38aad9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 582 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+7+-k0IEGZ*dNX}(K9i%s(S@ZmxMVn&F$Ft2X*C9jcNenUx5Nv}*F3ZMZeZt7-d%P%!zAqW*I(9!AAC#nGjeaNKlsmRCWFB} z*Yfr8s?6&i9!b{{H2IOfh5PSIp>O6J{y((}-`etVn%4gQk2QAlUHRgU$jepy;rgvT z{pUHa9|=*#&i73-S6j9{$lb>(oX|2&a^ClcFASVM7^UAgPzk>ONUu@0eV17Ky$O?c z<}bbe^_~gO?1SelUg|Y8ZFYDl$QaGu=Fzz8&w9-n=ns3-{3*SQrFLp? z?o4wE+jMF6Imvsj+odvUy~J#!=7=sMfX|s|H=u;YdtN_F0fg${~+s)o!Vb=@`LSf zZ%TU={IXxE;V@rdXY9**S?hPHS4x`&rv6MdH)M0i#IRtrMFKOJ&@Sm5%kjE zsiEm=-NMZI%RgzZWSI7M->WI{$Bt9 diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_forward.png b/app/src/main/res/drawable-xhdpi/ic_uart_forward.png deleted file mode 100644 index cd5040e39f8da2fb97324c04fefe7afdc56b00f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 648 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n6x}y978H@y}5ll_mG1~Yhur)r3^s~ycZZ)Rah@D@CG=X zn3ux;`+(gN1}+ZX00t2U#&bO-y#Ys*_^Z#Eey*Riy=R8;H(NiK<;*}Ek->~jI_--} z-5!@QzLx8urk zqr5tv#B1A*2W@XW|K{e(7*4;Z&9V)i=NLoS%Cw$$K6kcZ5T4Dy7^Hkvu;vW@Vpqox zVh*ZyVquGZF3{k3#}u%A+A4*fV!j7a*`BuY%gp54EZGZgM%`msyHsBbEH4)7SRi^} z>op^fJ>OiuFul}jIQ?^j&=#?i+)Iu#Tv?ksVUt1ypTpg#bF2$bT3@VYxF3Dm>0>%? zwTfAgK-d2|hd=cV3KN!^&bV^8N_^e)MH=6BvM>0S=s1VNK|SxERm+AmiEMY)SZ{Oq z&5@POU~b%UTg>53!}AS{?ML3Ty5IhhyRLu14#v$J7_E;k*qT_KmBjd-@reFjp7=W5 zs&mJ;^6%t(xHb2H=Eq&t)pt~O-nL;2_`Trw(YxQIuHAhS(eWStm=iCo~w(551eCLvnXs;)YNWxV7vSC?ak!t zeu52I7c`@Rl9Tpbu*eM6W16t%LPzw}*$g4gepgq0`7v>x9YftRP`W|}Eb&Y{467F; UdI+=${Q^mNy85}Sb4q9e0CIT^tpET3 diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_forward_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_forward_small.png deleted file mode 100644 index fec20183a9302d1c630ecb75218007591fdfd9b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 613 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+nAkmC978H@y_u<)&*Ui3y1q3mL2HH5!H|H!%Pk>aTzI8! z+h4kW;a^|Yo5Yo_6GM^(+?~!!vWSL!$u*jNfbC#u`cB{XzD75jCmi3MpZ@&YJ1Nhz ziyawQ9IzwP?q?hA-+ce=%#)j9Hv25sefzdJ{=)%Zo?Q%gSblluQ3D6NM?Z?TMpe#i zbBktJVzJNd+`^r`vJEr0avML|Y#4Xo%XIdJ5WBL8{!$74j0fDfea&*|4WE?2}a zH1F8U6eF;5%Em~s`}Hdtj=bbAza(GuLUbeZ4c*5F7aHktKFHXxk#R$z^6N_8%ijD9 zdN+(Sr`*25c*AI4d!J`@r?;*8hUZPak`2AumLJ#2{5~dkW7FS-TnxJvgi>vHuRYiQ zmjBlDga50|B@~Jx*=AKO-c&yAY?oh+%5i5`O@jBVIXk&4zUPYFkD@6I91Ymeg1nE~ b2N*(R-v9f1yXF!w-7t8%`njxgN@xNA4`}`Z diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_left.png b/app/src/main/res/drawable-xhdpi/ic_uart_left.png deleted file mode 100644 index c8c63a94445831649058654c90bb0819e5532ac4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 886 zcmV-+1Bv{JP)CSU*rfHPKGrIu*7H8|bve^pZ)n8WVxotfV=y&D1m z0000000000000000Duz?hr<9WloNj%+hc4CsPXsWHzfXBTgBGXa($I^@%!}iGM-jAA8#O6ojXe*I7FiKZ)LcBYvR&y3&ht?Nt2qv4aoQOC!0BBcHc$uC#A2sb|iiy5I-%vGuY6)S?-G2J-MGt4HneJ2q3#T z7l=R6BEI<%K#u4B2fO*6SrmV(=^kq1;&%e^m)gX)J_W6WcJo2wP1Hu8{$w}%%L3GL=m^ZiuD#BT(S=Q*~qCdXrX1d!dl7TC?HcJa-O0LIqxXOh?7S-7{_ zfcQ!Dh0t#9tc+|6Q&1nB6RXuZ9u$?)H{B3x560JM;9nBCJFu|fdy2Vo%yLf_j< z7FwpDmgaa4Lpqj(3pznAa3{zGmL75eY(-!S`mi2It!76-LjW=cAsr`jOG#djD>c1z z=OPcFx&2mPPdfx4e-LgZIf7Mts=h2G3VRo1hw?2(Kwc(-9AQNJOBex#?WvikzmN@Cs!>}hMko|@W@CVR?VWX~v3>mdMngRl;=E$q4K`>~q9#3wtg2i&)0 zHd^13v3w#n3$iWj1|uNjsWBh!d4gr zHQ$YMQ}eN|zx4a&ZnKL7v# M07*qoM6N<$g7=MxfdBvi diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_left_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_left_small.png deleted file mode 100644 index ed8ac91dec4b072dc40804c4b4178b065f7ed708..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 744 zcmVP)0=8N@8J`wu9@%$5uRVrBx8ftW!IF~j%BBb>Mv#M(Z}`;z8v z>q~mycWuAdYfB}CLZMJ76bgkxAqlNkt4qaoJ9|ROcW^o9p9_@XY&P4tXlq{}`3#qf z++S6c<1|elR;$%})B=Ef9ha+ustQU55vIEqArVHtZwa}_Fj()p-9!laGA@@WWkbjU z!sO<7JdXAU71YH0fFF zu}1R+4Vfqf$l2o(x2L9_;L{$1Y>txc{lXr-h!AXdnC+1!y(2?N!-qWv-5#e@*pefB z+2hp3xd0IYlKW(J^?H-V`$EC#{tFG)(%n=#3M_NLk z8A7Tq<^xe@gjj?kd!*svWUE}XOq|m^?p};^1^F#cRcwz4g>u#Ui{nP0@$h$yr cfcGsE4@0f2fN4>R+-;DEr>mdKI;Vst06F1WW&i*H diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_pause_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_pause_small.png deleted file mode 100644 index 504389aa05f3d9d05e1ef140cf2ee3bc91b61012..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xrg^$JhE&{ob891SgMt8Sfb$$z_5|MW{fr4xx;kqb6Au37 z75#t0d!2G_w>l$GJq#qAe4{(P=-lz*Ilac-E>~vmJRtFhODUn o98>#dK5dWN$b_u&$1|}6hK>IOnpZ02O$14Jy85}Sb4q9e04*FsRR910 diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_play.png b/app/src/main/res/drawable-xhdpi/ic_uart_play.png deleted file mode 100644 index 97ff9b07714e300ce171cb01cc093b9f96d04d81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 669 zcmV;O0%HA%P))JiLX{;0~P=U z&;SkCfEFO{(!jop?`{Fo00l?^6o4~vCIMwxP1wCZb51hx&5${{_paX#V^skF00000 z00000z&|-04!7)rtbmuW9uOfb;3KT}u;%PYKa+ueZ>)e=3G1-#*ye8PYga({ac;xg zu$FaQf3oE=0y3+xnjm7&)(_i)x>$tu8br<5dYKA1W*gRXsD=#!WCUd1VrwdOK4=gS zi8G-oaXx4fklCebSR+72KxUKX!xjND0*>dyv*)%nQU3X+>9KWz06Ai;vG5veXm3GX z^Wn+MW2;30nI=6knVt_5!=HVg=EIx{kf{I{v4$4`nYJ<(dwUa*F%x@Ro&lFI6MK8D zfS-@8E3NaP1OXY9v3E%VBKtHOR?GnTSmT}w&@mrGWAD-h9M1=l*t@hA)WuXP)H*SA1&zed>B0TMnFG<#@@7`esb(RqXOi)dhAUF2y??0{E68AmqQ#FLxAuQMu6~R zC<1;H&!;|)_5J_>r+q%QnuZ}fK4M{9=>o*#=u!pj(#E&^9WZ{MSM^G=BmpX4%9kKO z^|2Js0tSl@5YUgAqlw-Gw3V^Lxn2aAJ)Uk|fJl6RfNxxUMaiN7S6|YzBw*B-7y*jo zgOUWezp6m1fMPD|&?Z1lOkA3PVlQmcAfV_g%OnMGX`he)6EU$@0bJZFB%rOhx>!Vj z%lN>80P``iN%#j2@&ed+00000000000020{mjD9*W49%z7TW9&00000NkvXXu0mjf Du!k6> diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_play_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_play_small.png deleted file mode 100644 index 7f709bbf146eea3dfc5cd496b6f9c4127a25c304..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 531 zcmV+u0_^>XP)N8~ZMCgBnV%n`$I`Fw3PmED7jW^P=nL8dW|fHqhv!n=UT1t9`~<`TqqRfCF#<4!{9809^YPU;qfm Vpkraw!TbOK002ovPDHLkV1h-P>@ENR diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_rewind.png b/app/src/main/res/drawable-xhdpi/ic_uart_rewind.png deleted file mode 100644 index 3e66f69b83296ad5960d6fbd9b0f87627903cb1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+m?At~978H@y_tP5@KAt28}sCBt_w^V4%`bEp6cB@9J{ld zHK4)FfiZxA)q`1+0VsUOT+dkYSaaXWcWpKI&-Bd``dNPe-Tw26r-3$Nf~Eo+IquGl zmv`s?()+jfNYtaVKlWaKIaf&je{I;kz=y$4rwT3Sx)VLutoFTg@cA>=YBfR+A|IW5 zyyaxSdgr2(&!5Q({ST>ZJ)G(7DZlmOpFPZn`a>JEU)!x0z5lA=JTv!vmWq{MD^Ht! zU$A#y>8FN6=5{as-FWVO`tme(VW392+I?H>aubCXFzL+sRrB)UDkqf#Sr=T3LKTG) zY+oqtSY^POk-hNe(Q0?Cniaoi-w9|46Q9+wPRoVKhJEhz)m#%BqUL|!WoJ+xnke*O z18uOzko`@CwT>iR+$G#Q1LGj*6UyYy_G*QRpn;0D<@TN%=hu?pmK zWJh~(R!q4e@%`ysmngRB(^v&Av1W$qZPC(x?6PKy?%Jw+=dD`Ht>krX2U&G`yEhAD z^I!OBu(3?nwArBFL_S7rq^w`MR_=uG&i_c&|a(VEq^(F(t3Hpp*_3cj9{D{v^@y?W;~`W{mgN zEq%+nDfs_{k0rWWS&OC~G@t%Kb+5MvQ+zNhW|T8D2-I*IFz^WTFBR9>&jV86>FVdQ I&MBb@07Q5j(*OVf diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_rewind_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_rewind_small.png deleted file mode 100644 index 27a2b9e73054c9860d9f47e834c69b3d3b1b6af5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 601 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+7=L@ZIEGZ*dNae&|B!>o@zwLZ#o1(dCOWdm_|Aw(y?S-# z`GxUE^BJ|f`C@aLn!Ux@JqunwQfTJe=Ph^grpcF#`(O1Q{`b~%aEkx`{qfzcCS5>_ zVc>=~U(LK43mLu_Pp2I-@xAP`Aa45Wry==S#XX-)KF)}Hm(jzL{i-wm@`bJG+m37U zPd0D%y0SZI=3T4ftHspsX=HfrwYx5DvT||Mj2#{U;iug#7^+e24AooPw)t_V9nmVyhuyS+;aNKx}N$M@5$gkP*Hg{f6 z_WGiC_43>g4#{#?5EUucJdXYDeGv}uKGm#o!`Sax& zkL#IOj{Fi#V5xup*O7b23hf1r5&N0Aa{qYA`)l8-SluABacnUCkPhIH_TJ1p!iJt53Dhr6Tp lpp=fS?83{1OWS%022TJ diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_right.png b/app/src/main/res/drawable-xhdpi/ic_uart_right.png deleted file mode 100644 index 4ac16d068467428764e48346f64bb54b6cd4ef84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 861 zcmV-j1ETziP)OaU$+Zh!(p0WJVbwvu7SB+;9Q?0d@h&1g8F1N{F=`d50p z&H(@b000000000001Uw4aJWW=;l!WE_ARyvYW!Q_N3kul_&?fh>;Kzp5kG$Pv0a|{ zdhES_#DMdOf0@M}{i4fLHeuJI0YSlH`#bSn{;tcT1OqB_@dxpb`}HlFFsK1>TG_;p zZDIE`Rn1rf&NCIIY3Qo3`!*UkX9Uz`8e8cid(%=p^;&*N6!l&F_)@qgyqFe8!UH3q z-g@vo#myW$OPJ*d%Lq6_G+h>UPiDQ3<~=b2>Wzdntt<+=kF3YjpmJDxe=m%uOAM%Q zLW-KDi|kd}Ma6*nEQIm&1B-C|MLEsy>%1LNWLloW7y*^0qLgqZf_VDLQatrmerOam zZB_Jwcxqw`JD9mrUJqp?@pNt>o|-Y>D4uQv@wCT)`X+Redj#$A)H|a@Vb|{T8S*Js z(YO^Ope|#UBNNtE<7u#NKo(OHPgPY|m^a`ko_2-Z zRaICtVL&!PT4AB0_SCz^uvTFaTzi!nWE8huSXnfYp z@(Adi>_A3ksY)<<_0NS_f2H-mc!T1nO|;SqxLp+gjyvJHakE%__j+up8Va*8KomDe zEs1Yp3d(V_6U5EWmY%8)2E&`eR2czp;^s^s{@AkkszyLgJYOBJDi}0wO175Gi?3-4GF8dYlmQki z8JaP`Y9&KUBVg~!p8wsqka$$4p#S}~mAtr>6!i0T1Zgkl;Y8eQR(zH4@@^d_T%!U2 n0000000000006YYvj77Ci4#V=Z}S*$00000NkvXXu0mjfWLAz8 diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_right_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_right_small.png deleted file mode 100644 index 5f304742f6b035d94ecc9bbfd781e73df9f8f139..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 727 zcmV;|0x127P)YQCYi7Ht0k1EPi|MH~WyXp>^jAd0~7hD?*o|7+zUSOmJ?LZ_^s!$gZ3u%7L@{I=7VP% z_a!2<(cVB@qEY}B;TUmjD@L`X^1Yi-;U)bXkac?RG$k<~ew|5Zb_7R-fV{idjgha>@ws{Ht;KCkj zQFSm8?6Kj>9;+mPy8VE^q9>%u-?;9XQRxD|Aiv-xvsxRx6znc_IpYTx zfWH#Qg58aMlJ_bA+ufNEGInWqK?Pv)KiS=j%2b%`t|x@t`LMen0`RADX~rk7pQv06 zWlo5c+MO@257h$(g5A+%H&VMCX1nVMc6aOB?z{=WIWqa27|$1Fc=Jme~K>w1^{LC0JpuQ^5_5n002ov JPDHLkV1f=sN^1ZB diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_settings.png b/app/src/main/res/drawable-xhdpi/ic_uart_settings.png deleted file mode 100644 index fe5fec471f17c23c4f274d2594361d5db6922869..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1661 zcmV-@27>vCP)|sA)KjZ*#03Tp{faCz4eV)Z@O%#u9HB!sSNq<#TswhjA`fK%% z)FMJK7z_r3!C){L3$HDGAt^ z#-WWzCUdvt^T*y10E2(5eEkAKkP~{hS5rPe2eM3%eLRc*_j^?P2r}OYnE}^sQS z2A+hE)#f(H$?id03q*4EQa<-wLq-Z?89I*VsqB9pJOP$Fmw%ZEaGHhI=Lm#p@(;?y zofIKJ#FRiO7SxX12LW3LCn`Rc zKx3DLNt9d>-?YB(9Y~e^96}}FU}pAZOt z0xO7G_$pN0!aD>wRGZLpsQL(M1z2kTpYRSQb>C3}dNw0qjF6~ zV@eTDEhcm4LrC5T0F7EU?9T|pR=_1T8S4|;qNAiTw(9=R+LxTXo(b1!u=ZVB@^kXA z_kJ`H0IQ|?-jz=ob$uwW6B1hr%Lf5b!x&BD44VMRaI2=}Qlh9C@p5Kt2ZVFgR>_)Ipw#YYsqB>=Kr9Ydt8O+5@J zMXuD?6iWBW1U+;*>>O-vEvCMRLgvF5+CyXE9p&>o|yDqK8)&CV;epQ@~U zEbs^XGcZ3(BmqK;;1(1+wp0Yz7UZ7m?xFn;fj<x8e zJ{RqGz1kq_lXhCPlz`2*b@pLj(9neD8qplVQR-o43Snm3Wjb{WGlMX<(Ara6`1neF zS{L#qy}vZLhUlSYmz$m$LyYel;_iIZ23_NtY5`6*6K{8YCkE}5&mF)7m@a$A;{9FU zMsouFs^?KjTb~GswJ zeK`+p(rU($g7ZK5s0rlvvz3l_j=eh1P!lm9CL@GCJ}m)9$M$)M zW~VmhghH17Mrn&ZDP&C4ukZN0TjEU&^6R3zQYP-+@eq8oMeYFz?O9Lg#v&({fS28uqHIV zBZTqbhuah8N1lBfaT@o1NvAnU}e*7 z6Mu~c1GYh9DTcoSaJFgO(b})HZc5)@8tk=tb92)x+yZcl4FlFTPIAw}3G#_{5JHXi zB+&Xrhrt~Ko*tAxv`{|E2M!^to71yp2LNFwDL#+r6uLP1AdU}rewa}Fu#A0<0^p6K z3jEW(UV$b+I!F`Sg=DE7bEO!7&k3NOknBlePLLeUm>g56@#F(ODVqPFi2wXhr!;>_ zGNuZ-QyM>L?GUo(6!pP3INu--`A z8UU$r5qa`|a{nwnJ~+vdCK8Et#Qc4h!z;q~siuStE#yx%+2W&Zi9I;71>i4J)Rd$v z7V_tFwy4|zs7M)`>H*6rakF%h z@{3ZLJVvszAuUQETcbrpCXJa|44`SPQ@&=h+WP3qX+Y08u&I#7oe)R(L2_ zN>Z84sdSrU9vQUAnrRZRR6c<2*&VmH653%4#IuNY)?$pclm*4h2_^0+$=X%;7S%O5 zD>@2X81V3mjU7HUO;KY?y!Hh3g~^MR^TMM5S-~xx#+pG&T7@OF5gk?&PFlVm9qt|A z={)M99!4(~cvP)Kkc;{%mH!%A@PLg{Gg+>9% zA;vkmP+=jQ#Pr0zK-9kzU#lSu)=KG`sSI$d@Gz(diN{cMgig)RNEsoXt6F1YMaEb~ zhNt51V2=QNH349r|CqHWVEb~EA7ZN893Zikyh(BYWhdBG$;aLd#L(tHMNa-CrFfB= zM66KD=B@98>IdKi(}csm3*}umLzyOgz tRnh7415chjdGh4RlP6D}3ZUNt3;-0k%aSxwCl>$!002ovPDHLkV1nmLGUWgO diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_stop.png b/app/src/main/res/drawable-xhdpi/ic_uart_stop.png deleted file mode 100644 index c86dbb158637543341783c839c001198c041f82a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XR(QHNhE&{ob7!Mqg9493;3La!Zi`vPI})aK-ii)zIK21z zg{M1evZ9!QYJngj)UYZcH1>U=&aU-69o0_`)E-jZ|8)MnviWDZSOgdtSsWM`1soVC rVLrIu6E9r+uy(0B)J})>?hT9#Jl1agI^tDnm{r-UW|?d(Tl diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_stop_small.png b/app/src/main/res/drawable-xhdpi/ic_uart_stop_small.png deleted file mode 100644 index 2b07de4c241a5d9257c1dad3ffb913662acbd83c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xnmk<`Ln>~)xwVnEK|#PF(B#}A<_&GD?=x8R?rIE(ee$=h zYeIEPUyzFDB$doV(Wfd diff --git a/app/src/main/res/drawable-xhdpi/ic_uart_up.png b/app/src/main/res/drawable-xhdpi/ic_uart_up.png deleted file mode 100644 index 5411d8c99a5fecdffc7d8fb15037f9470fb87363..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 681 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n7ln*978H@y}5NT_mF`|!^0ilbuKVQEZ{b9ux8*su=nY` zFMN9sGs!Z%OJ#^?V7$d>#ISXBd56H38#kUw{`23|bcOBad)D7aJwq9RHll)<3whhK zW0Zw{GRNOG+nB3+wsxJjOt+@tk8cK>*KYs5d~L3BvX3{@CD#4w2kpzYZlBbm%h@6S zhrN2nAHhF)LdVQFrG*6afB1j<@T&6GY~MpoEOX@lh*wLn%=?!(*+7-?VfR|@e~)Lz zZ{3o+sL)gXYf0pe|hBUulS;Wd+#X!@QE(2{`cpZu)^$2yQ$~gn(n(r z?3n(ode&?ohgA(nyD}0?&_-;zsRZfl-B~4;yFSy#4x271e-)DVl8GchD;@ZCF zYbN*{d%&NPt@ye(_(5;%HlwR4T>IW1k|=%cP~4g##J+D@<&O8pAG8iWi+u3xo>v`j zZXqYfo>!Cd<=J!c)$g7#?qr*}^wP<_ZznEtGU8;rKKsjKjd2E(CN~$Y71*Wz1Y7`^Tmdd;NaDz10ao;2tjLvg-jZ1K2$hvDmr)i*b&6>8{+tt+X~PT*EH<3V@OULmC(yw1*pUy+_)l5^|4J!DG_m=%x_7N$CUUeZcu3)d^8ZFn9_2 z3br5OCOh~P^driAAg4lYiB(9O9K&Y<-=BYz00b|kdlAydvSSYj{>}6(IT9nlxwb=b zg^Q;XVw^-0+ysJ;qV#Le^oVDN*b|}zJzgl>P<_962e2_kZI3Lm7p>F)7I(S`k5AglNG5OpvW^$L|JnAxNq=KdMKKfP-8za1A%qY@2qAR8<0#)^t}M2Og&v3LpZLdo=_BOP!MrA_;(JI z&ARd>LXBA)x?GPxIJf81qW+y5e#gwnSYfek()lO)6XrddYhIzn#by0fRvTylgQu&X J%Q~loCIGUAC13ym diff --git a/app/src/main/res/drawable-xhdpi/shadow_r.png b/app/src/main/res/drawable-xhdpi/shadow_r.png deleted file mode 100644 index ae07b59379f5fe0924138b29cab52307382a0786..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 169 zcmeAS@N?(olHy`uVBq!ia0vp^AT|>R8<0#)^t}M2I14-?iy0WWg+Z8+Vb&Z8prCAK zNJNQqer|4RUI~!Pz~Eeznpl#WqEMb$lA+-4=^K!um&y(lH}Z6G4B@yQdwe190R;wz zgUr#*hZoPBSm?c6LqdpS`ko&Q`TylqLPSznqPvgt{8|0bh%r8&eUr=-Jp+)jp00i_ I>zopr08LIT;Q#;t diff --git a/app/src/main/res/drawable-xhdpi/zip.png b/app/src/main/res/drawable-xhdpi/zip.png deleted file mode 100644 index eb96135d4e7a3af0c47d1585065b33a24ef55972..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86140 zcmdqJbx>Si7&dnC#XZ42Sc1E|2Dc#_zH_? zGa>-c07|k_Ivxvq?VjJvbbT)$h1@ItWhKq0`i8v2d(i{XMGWTUX-X1OMfO<9UY9l@ zzX>{eP4YrHQeKKE1#RR@j{Gq`wgF_2jzBgH1E-t2i?f@X^VY?^!d+;L+Okx;vD~Z0 zvc=M?NaAp=+{krz*?H`JSt|Wvu2v7R|?vqd#UKlPQWpFgy^RoK zJ zev06|YA~qEo`_O2-~~Ms`EsV7a{j(|8q$FKRhl{zS^by-?R~~Pqj^T`Zk_m0P4ooc z)ArC=BYuz#`h*ga_G44pXbYkbBx4iXwB5I!ShK^e}%iIUDckU?;IG6 z2;f(4M#n9w1FK#BKHtRUPuzLhR{i<75rvdF2ZAUF#AE#0ezvko6>aGgMob z38lrnsSsBb#;3lBj>R#cuWO+bNLJ{{q!$gGQfN}-8?w`nIZyv{CI88qGISIq8PA}n z(OXFHi&GYSQ#=C@z=@^Fo-)&n9;ja*!JxG2NhP|XpstQ+b3b zAn?gTTe)0A59A%vaIW+mOk|6EdF70HFdJ;U-9<)9}NhylgD5qOAMa9S*xE2$6p8hM|@HHqq>r=nG^*Z|E7pA`j) z{-ZBl^3jWa1<`g0gWe2o+U)dEbV+(sEOj)H?_Hk{o&%pbWl2I5YgOtIG+0yc)9$;gUzi0+ z8nV(tWHt*YrC&?SeamB(CJMPoZEtUn)mq@q#gh)_9;^L06vpwN(S+Ca1mcBlG&TK< zKs~h+m7?rTCM0&ESB#^V29i2<&uCB2-KMBC#F=((XLZ;qYn!Tk6$vy7(IdA{d;v)k zIX7RDkP4(7S4R@*^z+e&Cr_0lu#VbB)DMA+zVV|s9enmHy}Xt;7=9ZYeXQTK4hfjP zMUt#Eh6@v049`?L!sh6hN4UM&1D~1pT|q~?lep3nV?FJo;Gl&8iglzSL(Vg}A4b-- zl#{lm`jsYiGRyIl3pAPWr6IYBbkSPUlXiJ}<@J+x(H8wv$Gtds(k%&1mG9V2LvT>7h8k+ z_R%BvbYA}Evd_Sf=Et_QVa!c(N?>%=WuCSgUTgN_FM?VTdf*{L>au^QwnowLz}K?0 zjW{zh_KQf&4pQruv`{9MDmFcrf2<3FE-{HNAJ0gjZy2;dv~R855I!pzW5#3sg9dA+ zA2sMA@S=ck|a>4%7+sK^rHV z;J0C}j;{lFL_qHWPX{H?jE<+2-y9xnvw&#RlNzLKew_t|!RhM9&z>*{KH33i@bMUNY?IUDqqA0A3|d*MBv*G`NPc4jX5LYgmNpieBJLmy{$tlW`=fSH{>@SXB}G`0p74 zD*?kd7xEm|Kyq4(3$D+Dr=qeZVZ->w9f$PX=7=sW*npRpx2}X*%ZQ4xmM9~w*)JZ< zE=FJl;S;|DTCQSx^-am9!T5)|$fR1Q z8@=?eQ#`?S`KFor%m-1+8~oPMOMER7d}-9F@iEtB46UF}GQvb&)j0F{9LqUDC}S|+ zu4|(Az@`#6TwAn%U1+lfnrz;{u63+GA$5f> z&4~3Hp-t)$l^^q?HFp4^$5%*~;$+Y-kFZHzxppH54uTz7kKsB7>+}VDN5+$cT_p!! zO1{BbEF(NXi%yB)rQWbvt!EbfYwcYo#PQk2j4Ynd!v()Z;cdg>MKNGHe>J30HHRNU zdm^f!G-q8lXQRgqrVM3a%DIxp2Td$5Zy`2xTEY}}q$>qr4DqVNsF9-U68MHg6R6z% zqD_}b$xNE1FnO$hG-ivW1x07NaD=d+F{5m*vMDxLJgT^KWO3gkaaCu(VQ2yOMdo=P z%|RsTVlpCUzk8k*vku;+549!20SnwB#pj74vD*Ep& z^`s2H>fSoGzm}&V9$EQh)JhLu^j%qv@iN3-4{!$L6xc&)5_UGUv{ze z0|%LX0&yoF&_ie<=+XpO+xWoif2OPF_^dDJ#AXA# z$Ps+7x>NynBn_k^wssXRC9u9@eV3j$VdYZM1Rb7xm)=0yGNQ;ocDgDTN6a30Xdu({ zQVkJ0%P%c2h&Wt_@{RVkuPN$c8y6^0`dF;R1p^Hwf1=toUU{?TM9rL(8%lzk578vb zowv%Fu%p9}(d812xRUzQxQMvA>2<9G2plEHwht2m2gpZK_c#7X9^?Tt|J^R)6wB|w zdg~1nFX~uLVKy-P7tn3yal8jA+cLL{eIhw-SUWpDy|${zn6rq}=pIK<0Ph47&k2qs z&)6EgnJ?xwOA^}aF$9GHqYvfJzK{m!NOU4~UEf}Vn>&jr_~U5CgBwWRWFk@`Bwo{7 zoYl;?&!ROZ&;0s8C4jhDRJk}1lhNWlGHKDii_cmq0RKzH1onAZ0L}8Am*D1vOd1MC?yvl0D=E>ZR?_DV>#nSD?&KFdgH7u3Q+RNDq zByH(!@biU7?3(9Jx;38LcOD4ASQmuP--s&I##qB|vW9K7UA-ewCT&W_+k3{7yi-K> z-CX`W*tU;cxf&sFX1vAZyD6gXqRxp}SEbsLkfTOCS^*jmN)SCrx}F2W6l;7P2xYUw z!pz;a#Dg2hVnEfunz8HA^Nz)L{_VAXGkE=m80KZZI@Hd^9djv2Z-oXA*L+G`l7h;5 zM4`|3{dWwz-w~dbZxY?V^EFzj#lAT`V(MP>FF35iw5(^JcK7Jq; zv2?>vF6eHE1T@Ry=_SpG?E1r5X7ofoqN4ulfKLDmH=xoz_ zMN?Pub%{hp@_#<^>!eOG`^j{lfGL9eKT^fMHN_N>ReVEhL~Sy^*r))0#gLGm@Z^!miKKmM1V5J?Y!2{FOm6%IhCv z{Bm9luCM();a>UQQ{l`>SSbE(DX5 z)i1*GvAPN^titpEz0^2)I`<;#pncO{M!jsJ%b+2JD zg1*pciQ#-H+yL=C;Z3&$=Q{zM{FNBt{VLK8iN_zp$+z>v2zTxDMA#e%N8hFnzM7sr zgaA~Z7TU(XXP(G-*UBq4}g@OhZhW_`5 zdV_A4u8qX`2AM^YT(|;-mRYUu^I^r!$@iZUVXVx!8gY-~aU|WhoPsYdO)Rg6fCq@O@!v>T@pwh-(@oVCnO@}%_wR^=kDLZy*h$MI}(Nv7V}eZLzG)1QFWPY zOtkFfiV!_2jFus*v*=ezC!Y5M7`xPnpMIR)^l*9k69R{>(f(WkEE zh+_GEO5)mG}!Jh3{+Y4Y*)@Z*a{g+0)xbTGl(Z42Q3q`fS}Z zj~Scqo95QDI(JPMVmv&;peeaYA2Whyzv%;<%Igi%BhWaE{<8h z?S2z$+9K6pA)h*Y6Jq84wbo~mdHM2CXvkr=`d){r=J*Savp~PauCgi9{9Ohq zjL$y9+jX0ljk2j<$gxUFoJ^ky zoLzK9gXl6R{NU22M&U~k%9$js9QShL_!eUuv6Mi5uUz*p%|4jB%Dwv@Axk1+?&Kw) zF6G%_gqGywtetQF@5IKh8|&lOT2b@Jm6fkR8!;>PohJMeN(tONsRuYD(AMfOL`q{7krv@ zjZl)1_h3ZsZuk|~XRHI}!ck2~pi{FQO6oiqr@fcZtnhzBM^AJt=PR=7oN;5;q(;&o z(6RV;qET93y2-;nS;!KMix2LrYS34!kk=fpthCm>^)2G6HB-u=_gZzUtkTj(VdhE>wugChkC+{_XL^OuK*D}spV0L~X~JO9we z3@|RQ-G8>t)+knt7opBLz9yQ|sT5>Y)3Y@q_m4G57>FWEOU!B=Er3VBO&Zb=<0`kg zI#?P@3T^M)#<~q-5^Ks8DPH^=b*f8Qo^2kjA=h0vcy$;a?)5CdIdjhc>w(N*f@Vt* zJae=6_G)a4l`URna9_bHs^$h|dVs&iVP!=KV4oQxE{U^4lOk<#khh(F0k&%e<&D-kpz=v5QUQX|6-S)b{HBp^p?| z)7EJmO2&m&f|ww3-cky5gpca3qmvRO1F4dAFPee^#&;vaIZEldVx2TH)@!ZNnCQ&0 zrhr&mlNUi6Ld;V}9IdzHKP|LBQbs4ih0L-V!^IJ3IM*vlf+u0VV=^Sjv7^mVwEiy1 z#^SQAofJ~pGEskoacpGCbF_RLMPLsApA+N;?Ju+yPrCa$v`k30bz>qu&y=~wB&LhvCdU#cTbBnRonGa;diFzxZm45tF|09j!toip^b`(KrAUJI_ovdb%7Dc&ukR(w^*qN96m4u7Q0N71|ZgP7phP#`%g`BlzvJuGC~N_tA%9FhRq5le5A9YQb&W5@V`hmM*k6x}pWQJ6 zyP0$70WUM>j9rnFhKzHa>+QSsEw3S17ZkmKIE0U__fyB41|02gzR%cV=FrZ4cYCBu zO5ZGth4@SNRlj%h`}D}6gC6q30#*NXZ$u~{nvd-8PzvbSYI{Hi0XiA)h%4^B_5pSw z$qy}1>5#-aM}sh_P77%VG(a6v|DB$0iRHr^A zDJh$yO?22U{&O1Vli5T(=*|1Qf_BUM<&0Z2eOV{UDiYej5BZJz`!f11zrc%u<$e8u ztV)SnW3*4G{>Wf--XTznhxY9oDtj;z7j+g!lKig0lD>`b*{9}~?xAGcvR*TdoYBI3 zwIMWkYA6>QRNX%~sS{4avWAB6!G<+0_qGMX;Bqrku#W{oc;ERxv#1i(8)tQ&`tBMJ z4)fV2T#`h*K<67<;5d)*kNjC$PwBP9!PS;K{v6>d_Sv&yw?E5hx{6g($Up8iYI9aF zHgAt|L#jGU#}DX*Kk_vX34JT{cd@X_J-fkye|o0F`tQBh`^Lw`s-Sds^IY-l#@+L= z=duJj7zldjVvyXsH&qAYmPmsF(*p8`wy2xEH;I1ty!tzIHMe`eZ*MJ_%2M2}nbs0o z-lMa!U8d+;;n%!Qwa>63Q^Ne}l^VkSJYqozKBDB2T#Hrf^<;RI z^rw9PHWRZDNG8J-&xmL%cSKaR3xZwWWznk=@Mf~Yp5E=Xq{g=%k!q;yZC_O5o^Jl{ z+nHZqm09FYy%*((h4zTrf>n-I1qf8;YL(JZ-|8!=OBc?Im(%m>-h1dg!o8cK`Jq%6 z{{F~EA4`O$^ma=$vAgIs^4)RdTA!B=m}(G4KRg0{#lhN0T|9ta-lNPTnXtOuZdP}; zbRip6Fj?=LK<>~-p6O4!*?GSD@%b%h_1J^2&wWd#4JZfkIvQTzj_o`=>Sp(=mFTQv z>g!Z|E*tdW>T5vm)*U_J`y;SPg(K?*zt1eAe;?wI5oIrNvq*G^=0}(5&T^XnR}Uy-)tM$RJ>uY)(?5gNKR$ z2KNY4kB%7qiXyB9ExKu=GB5CGDp1ERX-_Er1*D@KfID2Kn~n#2rr5Vmc$ET7TMuk^W4uLTGd%}vmrGfU9@iC z*dUx7k}m#X$f1=pWj%RHQ_p$oLoJ?I{QJ}wQo_2}>`bNnu5_;Bu<YNKW40N)(m8yHBM=7L9=CfHHIeP@xK=ggd5div zpH5w`59^L70bdaA_;gwhB)3ehvb$}Y?vjm(z^^I@FH3MqALbYxvylJ36Q=S{XYs(?INoCEtOl7Ub!P$sW-&|pKCsrZL94by{jG4)+ zv@nXV;a6-}j1l}y;az&!?gV~O*Cp;>&&3u;T7CK?`!B4Ojn_cOvj2v$$P-nBWlWjxX)YT9Xd^<~)kyFnw)xja%RwO>=27*R+-B3N>=acGc~F(>gG z-ZIV<1;6D{qN!WOaHT8)y-sv!PYBt>>NT>NXY^B^R|i>(A#R;=U1ShqRh`J!c1Tk0 z)kES)QXcF~gj-GT&L;d#8hQ_p5eaZqsG!8;t2r3URmKMi2bQi0D4{k>DbxKYV9T z^>4#B{!q=yRu21ok{oI^Lk6?+EQ7sS)S>70{(ikCyXho*(hz^1`KYm^F0w%1spuFi zLRXFE0axemHWN@Jd7W?&lwSFl@geX(QnF`8Q9l48Rv#9sM{GA&fGtT{?+R8z$_3A; z#y@tu%Qx1im1pW?p#l0T^UbR+)KKCdVz?dJe#9umBR2>eRyw{kDTSVqlcmwch--wp{@_nKQdki>_6{m>m^=;1@ zGZwfJ`yte|%ftjl>#oV>9tX|vqhFXDA7Bm8k{KJ2!bGEV!ApMi?sKy%Vi$N&#)t1;SoN-X zN~~#+r>F70BDCSM9TGgTxN+fuub$bx`as8C8RjIW6oO68|6!@cHI>^O!(#MXID-;M z`htwHx@mtjCFCCD*D@AMO+@>oR>J)hQ29E|6%+aQnLPCxX`WAOGA3I-PobY;Zx26sDu!gb01TvQTv#e~*Co zVzuT%9-y7GHAWW{i;nghP>~G611sE)Ll~R}VpqlnL43&^`PPYAUT`Fh2Q8G!tAwny zh@M-M$YkwT@RK>X?fnUqeeO*py?PDLR)}YK+wAnhVk~QkvB%5X+i^QL^yAIh&Xc*> zDLYISC_Sw`mI0mL?$3wb(H7U;YR3)vSMOVhcr1pi5n0@o2~w}O2*kMT^Cv*cS{t!c zODfqM+zC_(ya=qW&|C{m0fA#H04x==UPEXj8RC$rOMRt_=o$<>7a+`Yi<##0G#hRt zA*mwnmHW5>QGZgc)C#qv;u-LyLH2P2Tw;GDDG#Uj#U6iWf{+nK|C8omw!$D33Ts$f z{el~kNZW=~iuk7r<+TJAr^kN!2EBl|)K^yNDH);&Zn27Tu0~jdf>d z_d!|(j{6bi50!jnE36h2K%K5n|hl|fgNG`E+;XRaFMf>OW#*- ztAWnP`@@O3xzKJL5iF282k#IJK_Dnsw0(1X3k`ZT=B$#&jS?pwS|Q@Lngbq%`@y0v z=&NlB*&&sSIJ(69|7&dQ>CX_LoXv)lc|>W6KRJUNBMNV<8uOc7&z=7a2^Lz}cbGwI>v$>tE3 z`+?zbDi@L_(R^5(QUyZSHz8*`sn@uy5p}*We!KYw?5QI4{?90w2dUu3T3%WId4Vf6 zqt$}n^%~&{=s^AOA|NJr^F7^D<>Cai9}8of+aS9}Fr)ljjS0o)10xKYT&C!lt?h;< z6`ff4ej$?Py1IgI3LDHBCkT?>S3A|TgQo+1JRe)rA!Zbj$s zy7#SaZ2S((ky%+;4YrP4MJGM@ilEv3sd0VDsG1ci_x0J6OB88(|2;zz#O}iV=K6@q z`inQIF%-|K3zf(NlaZ=u+tg7w{$npxn+0}`nkFn$q>;~Mk5t{gAJqr}KsbVOZFXwv zONBw>$EQf$jR%s9s@f{)yy#MRG)e?dvHDM&Cr`B4#ar-zSzcs$ZjnCKL!d?UOqXiI zd^GGQ241Pg6|fMn#j+-0G}yyJw3?st&4=YSPa?%ub;7O{;4P+Z5;>sune6z25l}xY zo;zm++!5a5en4Nb=->!ydXkBJVoI3#Rg!8!sK3{RuWoVWKherCPBp@HTHj39S9VO( zb$5NLS)uoo&_viGhMg7P^MB|L!S>inv*h%>aS{1w4@#Xp*29^`!TY)PT8Nw7tivpr z##aO*Yes{e6p7s178jPSv267~UW{y-63?Yk8QX`oZiGejO>Mf3>@Nsy_+8h)17#OI zptclxkH=kU!eITG7LYZ8Ou%fJa-ckxwzPk}siegU6^fXGMqGV!nImWb$e2ut?`uTK z)!U;Xb1>C9HAfD10qe}tr8=MS5JM@PAd?I~^JBy#yedLGPvQB+hVu3Ly%+m1=zhKo zt{4%h$FNTSxCCbL4<7(9elr)TW-d4`#HkeT$6FAddr^W94!h+@Ma%*)erV7{u^R_P zFy8}`q-DvI?Z5+K7bW3U#~pcq#01?*gQHh8xybj^OA`qn<0q`rQd;SGBvDsvLMz#6 z{q48c;VjxkG-KQz(lq1uB!Mpg$Vxxx0MpxoD+)OZ;O!f^{4oi8{X+EqY|7*BS}0VI zE`sjWyA~qM=j=TN%SK>Qri3--gW>OW%B!Duo~QZ~#W;wiU_{JCncN!502yeM$v-DN z025*pRoAN$FXcu+5OaUIFucbEi2hvmlFz6axva$tgD?EJ&36%ZciFg8d)%J zm(e~I=bCMm`v7dnwT63t()kc}`R2b5|TuWzTcV@YR!Elq_|&3-9B(aN9b z+Nv;EvkmMaf~7ih=!6GkAbbb#5Sma~&l@5dEe%gDM^8$S?@0{=I0>D?nI;OPD;_{qUiQq-7%Y?#|L%o(%cI9`d z&5IdwY#mh8aXVg+q1PpXuzsEqu_j>LQFvFpbaQi44Mr7EexWNYsorN$p~sb@&{vPn zu;h?G-?PPLfQO8LBlH&!qoBQ?Py_Wq;@=$N;2+=LoEPs4U2l^p8`>2e$eF)O7+3jI z1~>zvKo05$a-G5>^Y_s&au^TaBk4A*S_6-WIhN1i9;k`%GF%(FRfRLpluZ7)ZD+4$ zKi>+!`!UKJ5sWTuzzwkkxMI^egSub6_Wc&xV%MqV@OUTqa2J_!84U7)VYf33t7Q;S z-Zn~MC#|kro?ZsgE_>++BU3zJ=BfME#;E_|T;Rj&>{4NQP%So2T)p&fr3m}11C$Bv z(i94-i$$+IpkUPO&D|gx`e&aD+rGi|JjVUGXZsihR@8 zefy;C3M3W<*A$O;5{mf1o*+f zcT(Gg-58u5S53s2p2r+NPH3{F$N?S2D3dD)O@>weC;`U*AoI8(VHmD?2&?@XSvgaf z6RjI(V%J}yCnFV}Zf_u#bBP~6m|$9mw&R%)SUY}<>44Kh5FwQNTz$4Hm-r#M75Qd8 zS|rHO7l_hJF~#(neH&)cCPa~?gV0H9#&lw2ggj2!oL-`DaduL8U8lahd&N7PO$wQ}Yy6B=qVCTRIdNHN1FLTP!+on%zl;3R3?tFSKCLQB( zb`OG1>RtsYn=b9i>W;sN7~h_JVJd6w3So3{C6^I^)ZTU4P2&bRDw9jx!#ut;X1~LDapF&N-I3clcAyA<= z=v5XgpC8Dp3RUJf@}0Kf!@|P)Q@8l{cLD)_>tHeK^ z0gTWLN}oGA#P_y$yTP~q`0(?y?zivBM06OK2*d8PY~y3}=Sm|8MZlyo-jyQr9^_)-hlSz?P= z3KBt7{;3Y26mk2oaJKW*Wug$U)M!hjd~^NbM@1qf!GU`8@*e3ZwA`TbVurm$qc4M`C8f9k5JS7=lV5H}a!j>b>E2|1f{VRj8FZJ^M-&^+uG2 z{6FnnWgwC<2DqYTraz@%me8WySSb*F+PIJ>sbrW^nrCJt%xm7xIXmjdLV8X8A8Q^f zv_Nj3-3chL5RK4PbQL)P`#!}~tO5&A?L?5Q(C;0k6ybEPPkRaB=1)%ZG$@$qh*dJNJ0l z!^8Ng7N^(JfacFE(mpO3!QNxWtgWgAz091j`>(D8dyqh7lqd_rT8pv1*G4Az}1)8wZFQ zXkJ3d?@2>!NbA>>DuD%E5p5qWX}Q=)jfEf1Oz{jq!BYRzACpkWu*bk&uM+79+JM$v zj91H;<{g!LA6LXFGq3q{SOS=G+5~k=xRKfCb`&_KV{cdf-Dd?T2fF?WtLsFn#wlAK zch=wwKXCH9d+(QhC(hmwfzcMCdE9rJBFRsD+ORRSRh6|6_?Meq~X9@ z?F!>>tF|TtK1}4vJvG-TlYzg{&SK`|0R`!(vm3?$iO{m}sVI@0$dyX4PD@1 zcl===c+h;T?jW*RNFdMg5rv7=b&}fD{qfZ(?Ud8#Wi>vKwZ>uFJB2cDobdx3J4dFz z_2Qk6&sD0PpekEq(O4{893G#MZS4DCJ_S$2oa^d z|=H-s*H~WCNZ^G8zh!H1t?vuDuV#U7UUcFEx5u&}mH}3XQ4& zsPliB<^SL*1YM|WpuOABs<%b*py>aeqsquCA?1z!MQz$MSw{?!suD+LO3n_wW@>sv~$+J$>x;#woh;c z$nA86LDn#^SXzsQWHsv)t>wbE=`2w9;Z**}Ftg%~M)rc!hv+NExs0%scg932h&d8p zpFRhf_$>1^3mgTx6|WHuJ@gzXto!v7cY01Ey$Fe{)VR`X^k2 zl`C}ks`fTW3V;N@8j3Y16{U^MIR9_q5J#m+q9mJ{2{q*_ML-JgEy4tdYpPem{_c5v{$=5$`aVRf?5eABqZe{Pb!}ka3WyxmCQ)< z7TDxQ4(q5vNS}j)EC)PZv)|s_5 zGekpyRqG#gK>=`2Z~=aJpqQEYn>vvk6b7P2?~wpSbg)qF=enb*j-zj+E~PgJIH6Kv zG4R!*!LKp?rAR<)X7~TXsEZsJaQ`nxT?)5$_lv}GeAFf{D@qN8EgX$+V;ORk2}ve4 zZ^2=rh`k$5ECaO3-F40&-rdamrCM7F-jW7xOQm!{k-w5N+?W4RQL^TqGG-V9atjcNmCRC70iR9O}2&e6vsqY;@r?heny))P(>4& zRPQJ}!cC5CGre0O9b*EKh@b&4+l2yfMAHUIrWXw=B*H<|MG^w`A_dlE!}D38g3rJq9R3AmwuBGWh2!9}&RDk4dT4XZ}!tG3T zAhd~6+dOX{A+;5)1Y^^s$(+{Df>XYXCX~LQCQ+g@`+VGmggfAU(2RDuSt97$hDY68_ zPFUUJ&Oh` z^w-x)f!}+?R)qD$W*WgP_c-u1p}gO#Xb3*V!VL!cG}H93#qHz(ZaW$87!_~RMYoqx z-W@S-pEn{4CI~)RPOM}9nE!*b=MY=tYTN1PZqq#|^d!~(e{N#tDYD;&^iz4OEISRc zHQispjz=8)oCg|kMUQ|6yLqR(;WVB@ithDJB8O$p>!#yiM(!+iZx=Eu0F?g!-kL4Ks&D3lsmF#%b-Sk0U_{J(?f_;~(r z=A>WZfooLka?#{4fdTyhJO>fZXQZ@G(>D9|Ji%nAuf7!fN>>I9o_~65kd7Qh{=t~{ zH*G!cpr^KotijsWJ*A>Wj*+eq{yzq88V$cP(;{UugEJ!V&H%OH%LfAL}Lj1KfFL3nrzB>N3~ zQ`s)Csz1e)*2TuH__S-D5DG{7#eWNG{#iWtVei2{o+T18f4FAMMSDy8#@M%B^m5VJ z@v!~!z|mMKes$KsA<5LU1577FHBw`hjteRnwzC+`-G(FaBPKe?;K`m*<7(#pd{AvAa?Zs$%Ia@De&AX8*46t ztC70DiS54u9Z|8L6>KOUq7P*YHA-j>uEp~>k%Ou=na8-kfbmHP``(cH00(r~Naxwg z_b(sMjUR~{7oCPmF86Q6qs4EmDE#k^n+{rb<&FRMk)14zo^wbuY^3I#0Ri%jF50ai z;{vfBoxT6{UySDlzA$_j#DM8B6C&DAjKk-?PpR6bz#w_vG}+th`zP+1wlADI2}}P> zxn|@C#7$WOk^p;@@mFDmzkz7PPiSB&yQLLa@u?L(Y&!+PQib{z&(p0n(3yFnMGgc( zFSfMv`x7_z~4wcOD(NJBo~9Px3qvoV50xMY54_xY{Zs>VAWEb^MN=tsWo$O z0R-f*l?Ai~Y0avR?SV(g>N{X%ZZ{d}iYj0VfkDH%3CuyU_hAnHx3H6PDg1g^qk7`od?S>z>-u!R6q*Ds(_?%@cm#QYHtk(ZF=z}K@ z4*FO`#1zn|A&TXG$LIcq|DlL&oUA6>l2l8KY-G;?S!2>}p;TwFWB0tJB0+~Wg%$wk zkIfc1spRoN>2$u4LjJrgQApWG4}_Ff1%!#HP*i4+M-l~{iM1Zhdf1S7-aIEH*M#A{WKx>e61+MexYgw zZW<#tyL{;Xc8_d8*qlTpKa+QkT%@Y3uEk6DlW#L9+z&f;{^@qN?%6-sU0yF;&_h~2N!1G3Q( z|7Y_^!W!3IXyO5uS)TIuU#a8-_v@yrKdnh84w?HroMywRc}UekR;nLZL|)|v_<)tk zAxmur{2J!mGLN&AfJvx+2m+(X!#rME$)nrqgh;h6DK+R$I4k-rl7_g8}K zpDWYE1g1`ddn+k#X z<=Zxo;~+(m_N!iwK7-|xXFwOe0>w{`M&Ggj;}BTuLLBHpj$KcIO2IVj5mYxgE_=dF z9`3F;w)zK%pl700<&Lncs73xI{$JghRTXc=$R!Pe_v{%RG!=Kvqm6%ms|B&W ze%}3pME;MPdW0NmNlrAY5!c`$HIz#t2281mU|+8IO8~12wpB@%-yHi8I1-od58Pq4 zch_V7qa01XWBLG7i}7nf4YhiRv}6a2=hV(4d~=Zzl3p|AM!`euttycMfo81sZ_0H+ zTLGcgr{`T)&?k87m0ITQ7F?C$>)P2do=d70$1|W$^48D%IDKkc+=TJmGk2!Ko|~TV z_tz4)xbivFF5B_kgP(`R2r*5~?!yc>tWq3Ea6uxumz~3)A=1Cc%%6c;jUH^yE@;7p zcTz;L$g4@*yBK<`1#jF)oY}iZMmQHY??z}c2x##d#_>;INA~bcm$n6j6A#uXccW>JI@{XM4U|A=N&SSs7a&)|<^4|O5v`}X~dFd+z zlC;+mZ`!0ErX=nY7MmP5s);VauxoJraM@{GY1pi#md>k`JY?SNvZHk^;D0J`l5AkV z^QRw_*X(H=fmi!e`FP;oiCIe(;IsFm|F5O^De3LPRUEJy$un9FeSA24?AUGgyB;!5 z?x%2${KgMWf0d-{v^kh`VoH6RYS`&_ANG*%0V-bp3ciH7GZ!BYqNGtT;%U@w8Vplcjn4fpMV|gbW~)l@^KYSLePxb zec7|vXUuz<{>}Hdv`nh$4B!PprAadA&qa5e9R4i*>%y0bwpHzV9mi1)iJ-J2*@R^9 z`V>CIGbXAH78~uRZXF7gu8Hl%4n7;?tyJ*(D&GpIdk?-s^{^Q2E&WqAygQY6JQedY zbo%{b;{@{qSU@^~KQ$9p1K=2zZGHOQ;b97>C=|!V!WvL6qgNkS6q>+4$sj(zD@FdjC;sjjEB)%Ry_-EQ|Ahd`m(+m_D-SE&;I z9c{ants#4lpE22Ev*6hCV`-LIETIHSGGY;QrLg=2MK6%5{~M=3HSd}Yrt(1!pEb;G zQ15xYnKC38-FmUn4l*7yWx7av16wG@qBjiTz zBHn)4en@$8zqqtZ!ET>f07miKt=frWeB{8ke`V!;e|-*H55-ACa-r_`=W|19>mzoT zzQ)VGmrLGvl)qibX}#0f3>qjN<6?ZwVs4&v0qjkypB=Nix%&1)w4dgYQi&)4ktnnr zE$gTGM)iW1_LlELz<*BByiic1BH}E1i&9K4K4LLNH6;7RFuz_Xd;cMW<{cnH7hSF4 z`iBX$@2H-DX|3jeq3J8*qHMmuNkOGc8l<~RTBQ|`rCYkY8w5eRJ0(TBmXhx7?v@s5 z5O`+q-}8U5AKwV-9Ld!LZ~ zl1)84obTOKwyQ`lI{)%#F)?8GW5N)p1`gXY>rTdF#9#1 z>kZh`W$oRJy0r#g|JoDz+r0KrPB?P7Ovcvqn@afcW(({%Q3cUk`Chh>L8&N2D^ z$6kJLIz_@LGaY(~9KmvI@kJa-#eKxH0B$}<*ynJz?7t7N=KS|BaLPFDjb>I?R4Cf6 zwd$VCSG|XQK-1)s4IrazCdhDAiNUb}UY+W;wzYD%JT*GG8FQXRDowgH<6L3b1|X!1 z1M=hNXuGRowG!BGy-J~z+trH>r8@?{%674KK<_$BDsJ_<^?KncMp5{z`|Z6Lcx=ak zI9lk4wCG>#1xC;P$G)d6iYNC^>7a5|cLW1operB`FP`Zo{%KV6Shq_2f2Xk^18?VW+P;YFInHW(I$`j5XWB!NF`I&K}XTTSMx@H6O~W#KhEkxLSQlgc#Va2yywfDdIENyvl1lc-**`RZ~pCiot;syIqKP zd3p3#%f-@GrNSwnN>*Q8x;7^XZae#7Av<#59s$m6#U)wZX*7-ZgY9yIk|?l^sx7~K zEp;gr2inH3sOEH>k5ZX>PmIkvhk!KxP08YZmLovP!MYJO(n)-2>$8qWxt z9$!T(eGF{05xU%q@@FQl58u-eF0>lwt5`8Q0i8|*Y!&)#tw!k}Og^8}^I|P;xzb8W z2-*mx&x&cgo2h{Nfd|WQQt&g%2S}TFzM0fLWz~AysuiAzy1t;I?A!K%_g4pBYZL4P zUL`T_vvB=xGqHQk^f1xA3@7k<9Ay+Cc}+Vv4Q6X2_Y(a9-5}v_(~F*kuB^7{u)!*v z-k+)^S0#shqTdY*Y}9Yy2vri7Y?Sdh3)g_L&8@#ho4EZZeA%T?-1tm*m`{B;Z3CQX zlV9?Mc1~<39w~R_=L0@LSLOFWEMu11n~!b9n9Jk(pSH&6x1{NAJrLeX^@JO@@C!G9dly zqg`eA)gAO<7u??KR~h#F(AzWT0vcVjHPVoI8+$M;+#BBh-T8agN6`nq5%1x9onP*p zYGqn3efa7+oR)Jlm+Wrq``X42jZf`YA+0a4KL)F(eWesdrONvH_X3~iJvP@n5S6o! zCLTdiLPr2Bpnea>L|d*c7|slI`xs|#=%5vZdWegeV^aXy*_DzL$+<|DW{0){hNA^7 zMy#0gl)`bxkMX6;GaC>4T}sKMGOh34WMTCez$jay`mcaU2fYodO4p`QbHZ;INAX$sn&jI*aH@1wUlpNs{nwu_x_ZFP!6x||0G8Lie+8w(H^qpId|E+)iwrWxHng)) z|LJZHs6a=!(w9w5HZt{31hK!RE&i$`6n=b9XaWbNS~^>sKO0Zpxth^W5+c9nUwFD0 zscyRU)m*!!iG&0>Z&dY>$9w&yxCO5C%C-@Y8Q1D};=f&b!ea_Ip$UPDc-ito0@%Q! zu00iC4!S7)r>W3)UoJ^xWw_WdLNWRV&q*g!uSYVBK6mSKz9^`_&kEr{rOb3%Qnqc{ zr#maN^LYvLa*MP*^{uA&4LBsawXG>zxsClyohXl`U8p*H#{rKuw6o zNF%b2r29&eX8V*p9)XV@#=Lq3L1mbEyS4=$>tZRbTU~d**~@2~Fm)hY1#C}^4gLbV5M3iK?1UXu{R>fkK&U0a% zxgHl?;k7>gDV!^?$2N#|f7B>hS7#hH&4+G5ZMK@&qPEtX*gC7RlD?jLxMxWZTyJp( zwL9qwbBky{Mso%OjFB+3<4n3#&7jV7c2k~S2sdQvH@M^YlT3~US$OQjk}FFe@&R}a zdGl39AvXZ+jNPwZi?X%eY~qa%kByxVDEh_$R1pmn%=$8GdwUL_Ob-3t2P<-Ze?c@D$sDIkj0HUKJ-kA3;H~eG*Ni76e)yZnei*fzg>MGV8Pxh#> z=?%S}uI!%FoWEm;aVK#>mVH%}_3o_1d9-+b`}B}~_9 zi`PQEoqjTh*TdcA{-J`Bjo#5>9kbg4E`DBK-u_`A3MQLJMxE7MVTIf2s?YM$W!l`Y zV2Xe1C^n4s68`@{0T~S#1bNaizLSNTJuuR)NSOFnb9{K>A(zSzv`2#i3MO!FpRwF>f`mSGP>|;HGzCxg zTMp)xoOzQ-Q%Fvei;t{E{yY&^JAM80G2`=Y2oL7NoMcV&qwLu7y`)I<{b7^mdPtM# zL--dDCPT??P~NqqM-}1z15wTK@5!%O^8z3~5=rhmC3u*}Pln~9GSVvH_1xt#gi+9a zEv%DDRnep>O@iRariK+ICcvg*eVo$5CCY1NK-*e0jF$_~r{OsSBnX#6lW(599MbRa z9?nr@&)_FZmFe!Sq0(k)H{@2Zg6&S;&NJS3Dt&c&_ho$KT+4~e=8*oQ#T5A+EpauDhg7s(6CEKm>@9$3!c~3h6QS5#lRS#X5B zzGD%5?kmATU`14!m-vdV!V_&d{RwR82>TB?nAGW*xHHsItM~O}0x>_N+ zdLJyd6W$5vb<5(e-Obh~{jx5AYyhBsD|_!V*Ntz0PF+nv77=N)G+!9T!n{n7l-OiN zZ*sBdpC-}38o4TM6`AQUtiUuLWKvKpXyKnSNxYUn3fX|#t2J>+aEns8K>? zjp&u0`*%VTk}2>8Kg!FWHIDfxz-sz2-BZ^3L~IV};R+-45J=r2|tXomO1tbdi6F*{#BArdPDZ zcX9=eomb})eU7$vSNDx3Lj>hb2Tbtx0G=OaKmFTV2Km|hswR5|pu3&w4E{1Bp_RH5 zjAk*l)Q|`Yq@45|tDHUYk=HDtx=dRbWxW(jQXX6Jgc{dKa(PyaiQYQQ-H|^9s)LOz z{&gs2REJ}Q#Lj422>+w_xOSU-*FPp&Xm;{|B2vF6=5oBw+QfPPxkDKLEF~OhtsL)$k{6r-H=Hf=7^v!2(}rHKfBO4osyKYOR=Rdbp}M z7mO&%GqyX=$QS`RhURBaF49lJz)VX-?LY@}_wF6Qb;2>-*p- zxD;o+B;ft~Jw@&mE%7>l6tSaf!&+{wlF+> z+T3bw6$XpoA}&lqu1Dj^dY%^AMvK4PN38S^yYIfU1E?*2q+jcKd2r2snF2c&3i?kA z*$Q{f-wOp$CJU!4uZ!F(H-Nv#+1n)EdjfaRP zv_Afp7E5dO1;D1q(S=q+Bfge$YI;B*b?zOQ?qIk(1^>H}S6mB% zIZi=xe=p*L=-(cAr3;H3nK}i6yw**(yIe5^IK|@UlGQIZd!tBopLF2o#wvP zS~PtQhT?{rG-j`dpjATcaeE#PcTo|n0UmZsuFX*c5uBfj^YITaIn) ztVp&dKd|H5c?@Y^@XqPHCk*~|QNX?<$?h2|`tdpN7GZ=wc0miPXgStY;`>coRC21>Q`T(;>URYSz09rOZ0J8WoCV0pK z%)J0%M&FdJ-IXEFxed@Je{*X&)D>(rZv|js7l`n4u)opO)kXToF$l)VWcXYyNGNq< z`u;%#{UJpFMZ`2$z&1h;sDdv75K$(L&JWbv(%5~^!as=8!LYPD02E~v%_oAUW@e2* z;Ba3U(kt}aQTIh=q^)1|WFGUqs!{#~MVTzvnw?z(LZ=2!#yN|~tL9VYR4rG8 zGFCM8Tcd!=xAUCte|@YsC@aSyd36%>X^0CE$eYBq7@rK#@sFgDLt>nW%kTx*phEr2 zSrwK+m<^~UK|7J3-yov8-B9`EaE!1R-fqF7&Dp2v4Bxwbdj3lO{h!_RKa@2xJ?Y^) z{gd&KX!o$Grxhe72ZUXVu4Ayi<^L0_B9)QYAj*l4pk;a4ycLm@rcC#lrBNFXp;loo z)g)>;%Hlb5QpCo*>yYQ?&@SomV-1@xL?8AQYurxt5+KF;8fwEY zVL{meP@;#)zqVjL2pH`s-n-p`LD3kyaiE0=?oA%>Q6oT|B#jN z9L`7hoxsO!WY2@V?b@qNn%zI)&Evlr{Re!kB4Ku58`%=&o6~j61d)XKVo&VJBbQ+l z5M-y0*97nWNc&w#>-b#GY{t%y;ga>$w7|mo1+(5+*ah7#HN`EeqSxZoV$waJto!HM z9$i2lg0V%7fV8`SDV|%4W_vgAkUrtuH9}2)8bLUfE4`uLSeEJ@OMXnW^9<}O5#t^U*ja{R( zm7t!f#i1z4^2YUawG76dvw7SC=)b@M<^$yg$0txlP#s02yCTp8z1gs!92Br!g0B1( zb*tp`;8Q7?7

ce;d3z%%@d!7!!FX*-$v?$7)bmS`2^JHtYY zt=BsCyL%K9)alUNVmi|jIN^}Q5Wpp~ zPBdxPxp#7%XOPUGf0Nh=$oqR7;K}tgG-f&Ow__U2v3^4b-d+y1Yt86 zYl*h*7>TJ2tU0o&wt-tsjOln%N*P+5?S1TtF%U?J8b|;PVLwu)m%QuJngWvLz;Ns6 zo`XwImKW+_)z}_S|H_-^8yu)Xl3^TuEs5uVBVi|x6HXJ?&1q241D_8K5YnW#@mET6 z!3lNJ{ihcqF4ASI$r(r^d^E}7YQkG?kIgU+|5KCp#cnKkB{{*Cqtv7nLlvKsnRhdM zW6XUiESpBB$tWf?OpYeCjwao8JoYL&C>B3u+=R&(f%e8efB+!~`TycWZH0a4Oc4qlKYZItAzGlMcS&Pk(^mQ9#8 z=Q4y56{F=0T{(q+FX{m3Pr_pvCGPRumTe#h7Hijxdrp%PK(;RIN288S&`25jH*C20VKcQ z_r%?0+DAcQJ%;3xIg-)ihte0}Emec51~QuE68XAJ$;wj93%+lUP_t0S65$Oc@szs5 z5M&>I=NZ9EJ+v!g%nO?8Dr5S@|4z|&Q&}p8lYmp12tAP%pcFG`x&1#cfOXKHN^i^( z9)IT2lop5-2OG5tULgH<+Qsm3OVqM=3D&s>n0XiQ;>IJ*;A_`bYAfD>$X7FReHQa0C_ky|&a8S;!Q*CnVhHPu!;&b}Q- zBPntx$$`16DuwiAt#}C*>!JCA#8($%f#@)V+?=Qv(S}PVK}Kl?2RL$xs3q7_*SbZj zgt=cs?4|7o9qw%Ht*zj*6eaO57k|}cE!$BV_w-}dGX)? zS%}W~!6AO|YXibUv;>=_Vh)`&7aF>lMZ))V7;6^#{D@jO=qgt}i_3gfNF2mPl&F)( zgSKv5eoVW3U~EFaP!)B*1~HzJ1U5al%wP}chX;pn-|N(H3NcJ^iwP?uCs(Q3z&xJ! zb8-s>6)vtKT)w|6Z8Q@J2PX>?M(YPQpvfJ%x1nCZB;C zTS9C77V$>w^#Nw!udhG{Eg)d(WUJ^YDed~(clK=d6LA*HXm#|NK$gnV_V#v|Ab}Dd z=qS@}TLu9LUf}tk*IlDD*u=Wg32{F6S7(2^Fu8eoC082sqa!1$04$oV(2t%JMCJli zk_R~oCi(W`{psGHjV`3OfG#z3c21a>Py+J}lKHyPzxV?|_|@7qU_?KV$S@_!1sU|~ zx~=pqFOveE(-1T}SJ`{{iXyAEo&s75A_oFflOh0Y#klUX0Zrei!UXnHX&gx5ir2Vi z5m+XuW{YrL8TJrkQCdAKrOCQ>mS4KD7xAJd%5K01nd_p^LVhQ@b3GsGD=+O}vJ>Y= z{9~5-0;9%nOw<(PbZdQssZOeWc^W=A1SKVf(C~blc(zP`Y|-H5cg*i4UOyVXrqmH8u66XxPjBWxCU;Mcs3`L>vwjUVUO_a)>wuTfL7)jGad?1i8;gtowC@txr zshv|;c(}V8$$$`2rUudtCO0>?KvdVkOvzSZ^)nS9BZN1fwvsT?;^N{K>rYMONqejZ zpaAyqC5*y`+7#iNyRG@~%Q4c?(=WlT07hY<$_S-Iy`uN}!MVBSD4u7=@%i`%!oqy{ zP!mlH?L?|DhRu8wS;N0Abq`rRZz3wy*s7Bt8U`slXZXcfP8&&Ygb4d_Amv1Pwwl_T~FTa*;$N+ zsmh=(C-6vF5a2NWg0`f;7j0sBY=Z0aZdTibngzlT8N37Ym#An~c^g3Jpgyd!nqvdnKo9I^ZI-N;pppCg7UbT4-hy%humkU{ zmD^fv6*^ z)2SnY9nBh(-wZe6s8X(ZJH$d)MlbQq&lpZyjMcqlx4}lt`Puw-jkE`@%Tu(_PIhX2 z72SJC1^*ji-mZu1)4g2%dvv)S1GronE+PFL%{{S161#uw30n|MvceL+Eh{MovWR&d z;_kEFDxGwh5Y!2t0wLlJbjof@Em`9&j%p9o7zxxMq2c8dzZr=74?f+D4%X6@$}$1j z?DqL)w~EKiwcAWu_BJ4yw&|=!&t!vT6M1L?eLu(+h9%Xav86R5iE)D)O$6aab=adBfqA)UwaceW}SxRY}CPXgI8>{UuBt3rEDm2@g`# z*$qF+SJdjh^yqO*Gr&_S;e8`W|9qOJDANf$OH8JQ2rC$SMk|tfN}7m-%+%NzY0l-7 z9Cf1cIlD;q`}-*SwZMB#UUxB2*P70j>Pt8NA|7Klc>XHJd`4Wl*Fd=RdncWiTy8HK zlfbv*#6LkjSwgnEC{MSV55uo?G~o*RD8rE|eIM@9)h5rzrRWm`49zl&aN!d1-9Bt7 zCg9sPmw%Yt=GEkbr+J>PT&DKC3+pvfq*NfNJvl^0f&6K;c%d+G2N>0vpQ?bR4KFTc zgbi?+O#@aAr<*?pU?9!g0*HaVea!u7TlP$eIv5TF?x!(*u}Vqc^nr2eSTSgGz?;y^ zU)9Knx$pj zUG_9y!qMEp<+Dr`AZzu(S=g3$3{Ma%?1Wk`I%DMJaR%~;aEGhJY30ezplaj3w!lIX zfR6m0XJ~UBcqveMiuH6=v9_D0=ob>sMz}MexY&JjgIZ7>vUjJUMx;a0N5N^k^}?8} z05PEyXT~&!TYCPNLr~`R@%r7^Q?c&?{*$6Zr*A|+KG-p{rJCo9Qj!;%cDUy zOI0<{RCZ0eRCPI7FoeK?RIUKR^lMg2xnyQBB_ltV1r=d;cQ++H9cEz!>0w5|#Hx%j zd5Ez=*4L`0rUocYbYxbbb?<1cExY-o>E+mY45Zp(s<2j$4HdzA4dt_|YZ60P45ZLn z15dK5SJsLc!X+p;2v&rR$(QV*Z+(>U3CQeyo+5PwpMTuJ z(@cbD;)LHshUyR$lgaFp+9;FX?0-Br=^HkR)~YB_nYKj7ph)c~6Ao2Y5TTe0EpgGA zGr9Y%#Pfv`^~Vee92zDj7?PQqoo!}rAG4o#{_kJb{d|J~y4}r#`k$3_sQn22V-^Z| z%9eVz-wP7HC{6*a^`^RpCl&9+x2R*Ny&vXR1)|!LCe0)7rpckUiNhmnePm(}O}?AT zer0fSL!{DCq?&)LfmK4=L%k*9+Z#-Vfzp~fgJhgP2|n4Vxiu^*S6b#+BW^<{5p@#){Z?S3t z*qHEISq>zq2s7wL|1g@-))tuJ^!PJ0G&l5$vpYLw@7kD<_C=0Lw-Fu)P~0QymGX_8lARP!pg%Z6+&hiJ=U?={t6bf7&-h6bp6sb-voHMez<(cT zqSGg*45u}xq$EXzORfm_yY~lQs*3 zRY-Uyk}Csd1(49&+uQQNO1Xl#FT`W#ll$d|g)Pe2=n&*2Ce_Vj93WHZ(G}q37&ZI zw?w2k0_)XT#)FeSK31rwyz^(OOw2R_gWf_XA6o6LeZD|hPyam8yZob+ru)%15j5xL zygyw+Y|M0njI45Huw`Yjf*M*sOT&Dg z5z2f)v5;zo0-f@4>)_&V&8yZBB8JzAoenH4ukFcD%6LLB$c%kk-w{3=9wqOg(CqTA z3&vU(;Da!eFSleDi5`avKkT;+EBboUYt!=vz85VwGD;Q+GUt7o(d3|K8JEXO`>u+s zaqzmcQuE&n^7kJnMeGgvW<7x-uWOJt_8m zr&CAMON?}zp3anBeBsDnFgNtVw_f_LRYIle{jO{#JtC5cw6E9U=)(R(F&Oh@@dB9G@$33T!lhWa~7-%@of|3zEVp^ zad2}79yT!)e9Qd?LOW?6m=Fi^l3B2&W#35h~{tD*g;lUNd$w_8x8(f;iEy|?l^P0FCPEHMT2)|=_x0Q*^ z8h*Smi)l=omo^R2w(!&Ynl-8)S=go@*FPLDR4&(1Fd4^?z>~I~j1rokuV(kDgg3Q< zFD0|$dqJnm7C9vAaI+CZsn>|Mg@Ffgx5VXVo#2dCpl6;ecf^lb;*TFoHzTI_Ys}h4 z0$RXImS8`7EmvaepU-Pts0|H7OaF~P!|G+MZBJ^2hK^oFLtK2R%-uFL%9GCVIRwpJ zZtzK{?n7?ii0oaR$=i>f*Lwm$zKYc<>jfJj{ZX+NVs1oI>2+;O?)m#Qs6-R1Wu~U4 zmO-_y!i3M42Bpp{)j){JMHBxycStJiO*nTx-{itf!cV|>go~6Qn&9Dw{73Un{W(bT z&kf@Ql_U)NQw-CM90pD%2x!JsbLMhF)gjW<+L`M3$rI$M<#DyOZ*tSgCd21_ZTvN& zFSLs8p|fdHSnDJe35{BjGIE`ihOJuklf-?l_SQqo_)9jC!tp*)`GrKm(qz@!iiGJ~50dF+M13WuCzsdo&`-7ir!8wd)qpql-(q>V{O ztSyR?pfWVO{Jk6EPri3Q{S@m*n>Y1N76<5Fz(b7;H2?hRx_`4L%jR@`nL-pswYyu= zFJrh=wXTwfjgSv{%0u!78&REe55-^d?_vs6Wto$}exn>EC&@W-4bPulDEa$k;} z?P^9z6=Q9Vw7ZRvY0A1=^+FBu{aZ(Lam$phG%ZH{{Na$30}*UkV8Bift`kZ`P9Als z0F8`q@Iq7+IRE$$5-Q?#Wk0s!RO{L+l9_vbP}y2 zWeo8bU;GQ5ZGGWs>5DRM^Po$@y9;X>WE_o-5Pbnum z28(gucbci5ccRWkK{&031uR$$I0bmR4`%X6GwaNQudyX@%AAG74-nj|q!Vm3QNx_> z9Hn1TyR(SN^l2*fy-RnJ1%q^2mf}v`CfCpm$@DuZ3q)n#RqiAxSAyYg{Do0alYrYu zhWNBq$3aHxr6&A27Yn{E!yqJ;Oz?$D=;^>zz90jcVx8XWU$sx#+^AUX{r#wlK8sUW zrrJ1tN87QMF=*nJeCecujz~*z`BXjsfeZDIIHcNZP}&uvtE?;p8fKF~9yQF}_ikGx z6X>58HAa@bo*ded|KG&~*KWv$be)Z{RS(HSF!HI9Y=>I~1kj`49U zgqwvjvCS)OUzu_1$saSQUGv>+S1Ms{G{xU)bVIaUFqW!~=mzE5AxW1I(`Mc#JEJu! z@{!-kQG&(P)V&HCSuE#;fQ11M^4<|h`HT&v_ilehZ$(PB z-7^2V*7o8cnEd?RX&PS$FB`&5b&NnMav)xe<@D-|fm1d&Bw#z2z5K%;O*wE!CfxHb zO2#`O!)wN~x$t(cra4#FO17pcJ0S|4g(S&>qbg79Z@t?{LT)E}X7%3T^qJSWH#B9N z7sTQ;iHz~(HK2dU@>~^^Sl^7agWhHy{1c8k{KR-KQ;e0=eo0EcT$e56=PIVPP)c60 zkt{4ccOZ(R<;G!%9PgaF3D~PK4`AR^C;f(Iwe3kB`0n$9Ybom>YW-J}@9`ID4!fft z2HQukTTbs8Ns8)vtB+Hv97(OW?lR$O9Sd-ZmbvyPf;#^CWCsKU z)b@aL!wGW38!q0jhaMT=aD2stbTHtbH9V8`KsSkZe3JN{Axs~gC1V2L635Xpy%;u% zb0t&9Ev+lgnx$B;y1Y*g1v8l-{5APrU#KULqnt^yCZ&>%IeAnJM-G&BcZr27z3A)m zP!^g@aE7WkYtz3iuOIL9#D<>E-!I2iDI>5 z%&s{(k2}r9>fqlb^i43+;U5u_A+Dw5FL~s5WuVqOlYyhv`r^Da*x12OOihn%z2utQ zF9a`FzEzvJ)0}!?JeyHnR`;Bh6IdD6O)%_&Xvjn$M+5?(b6Y!1+zk$i@>wf;`H;*!g6 zzYRzko8aPR!(~REHz7|Vkn$@umt_&|j(>lHLApE?Y@^r#^mF-1nF8}?@J{Ed)_tAl zDD$q1jS&&@pVG7YUf^dPa`1fG7!Lqfj(G^(Uz1XWtzWRlMOu}VW@+^F%Mgt@RW35! z3D&C?jUh`gZAwTZ2!IiL3cU{67B(mKn}htdg+u^LXI@Ey3vvxK-JcoL+Ad>RrA*#h z3%Qof3F6z*8(EcEfU@na1QQJ;iD+MX5#FE@qf^P&9tC?Trf`=$3r|H zQP{a)Tf!1+p*(ee92rb3j%A-df%kRRKgM1;!fxqaN{dh@Pr{mqM(%lijEJalxMWIu z8*kbYfRM`|ddrL+DV3?!GTMdWT4j1HMMux?&{T3WaMhfenY!^O;C)qa*JP!PxeA6_n)i(^@;i@55Q>`3^mSIeutAm z{=}3EC!30!(a)WLw=!RZKD8Z?r_ci7t1EPx&%?->dPRA8E;?us;cP~|q+nkUCw@R> z#?x{HFK9;4X=-neS4?HPYdIY_?p`gylMeru=6RQY&V+=OWQ%Lc;5wu5rTVC;w_%3e z+65x5?Tf-jKb1ini7UgSw~htBFrHNZu0-HRIFZP1y< z5)l+@m2f&;F#LbUAU8BL^c3%DeV-v_cI+@b&1!97#5Hs3uAi9qdzh@Od)0a>&a;1p5-OnZ$C?}E0InmBr(o8+7QV7 z;5PFtDsC>fKz0zNP7<%`SL{KQ`STy=R4;=eG4Zvpu^nnG>8Y1bE)wKMNkcIh8yAKO zZ{liolXm2sKgr&qe_J^uL8qgm(`a_JfsX+Uhy;Ax-!pNxSQAT( zxjKoBMj8cjuY|5JXLuSAm27LYCPnJ&R$2r#`JzPzU%B`BvZgV_2|y#0!lby3?Q_t+ z+$hYt{NR88^IWwy*oB1NW0F0)YnD~J;UUWFk4SXrgi@!ezpHhxx+)%Kv} zaz>|s{W_bb5u%*W31RLD*ea(@bEBpWZ$=!Vc^7}K;zZLJ5fVvOy;|_D3xkBsSk1!d zasoGOu=O@$L(LG^kjast!dI#JI(PJqINI%1P(3W7s~?JW8j4px5Gi*GYduHaC%-X% zNy)nIH4PuNSaf)zxtKzl>B^=qhk(#`R%MM+fKS~OsF5U@PxF==fH5v1{tv|}g_4HL7UpP?BzI2BOW|VWrtMbNJ~m z1VdA#((KhZuU#(q+Jb^KA#$+&1d#)?xZC%Ovi0EdVVj9V_Tm|>&8jpNH3_88A@g|R zg#>$LP=7jq-`8e#@dI;?$(mYyXM^v2N>^XR%XSdTC#pDvz5yXJOaC=htA9jvq`?vb zkt_(_JC4AbP-TNsz$4`AK(Y7u_YE4RTEqE?{wE^hTotydAF2+zBufPIN&RAl$G!5$ z`*jxxO8x~@Nn*g&%-kDQ7rk_bS?GEV1g87fGGJ;9c90w3dC*jX+nK1mi7euAKJ+g>uIEt{85h4$_XW%t?amV1@e z)v``tV=;B9o>0g3_A}y6=s_vQsZW~_oB2AX_KwS?p(-_^47Hfa^QlR7GM z3!2A0-u*>D!$0fqSzo6_!603Sbt^2=T*XESt1;SXK~#YwXKQN<2rsW}q|{8!*RKOo z(ef_5#P?~;5=N1-5F+rOHgTPF2way(U${Z`V^4Y|Lvia*3Pc8r1%@0(Y+2*6zg`~| zNSS8&9>tzeesw;r8c)ssAgw9pF%0QD%(v|G=PKYez?-m^3lcIA)~1r`>%AG}e@bsY z`4y=zdTlMGhF)WG$oH-|<&9v7i8dMr`V<1L}iuP-i`O9`);hj>O<374KT3VM# z(t=-qYQBZQXN74zYP9*b2-vUtlarB&n9}XXVi@Su`>e5RMF@-L%Cz8D2~*0t@g?GW zY+5@qT}g`K()@m%bJ7}NR(|)OZ1voJi1zCuSQ%s;@v7gOn$c_L(mlrZ`+%3jyia@W zYgk43)o0}Efx9=$FaKfNN5Mz0!LMR0V+xbqQ0%?|!s+d>_OKXvQFAqmO5>>C!E_4G$Srz(Enc|8>sMgfI8sQ+WZ!)KN z)JOuGD8FH;5sk6Ke%C=(*(+k0w}#uT~WtZ;5H*SDTkT7X1s?hytLd1`7u6MA*?-YG->g*Ui-+752S zc$91D+f=^%y7&6*@I^L`Hj0kvY)i_P<41!_dTk`1gWp1?ujo|i98F)5wbOHl_Y=Qo zgNtz-Zdj_g`iWUOaq+RjV)*G}J$zX!oNT?^vD1fW7lb%GC7S2Pp#gHClnL`{I*F#5 zowe`B%?mV}eZSCWv>?yb#c*}vAQTwRnwvL#fuunOXe02T9p#yV#kQ#V+FAwg0%5xb zFTlXPdOz8ZMq!5qk7VRrga|9pz^eSU!YMG}`gI7ml^SIX4WB1R86ADyv}wmArad$xWLX98SCMx~@pl^WoNi zYR@R&X54+~bw3Eb%hh-Y;5)Y9kG12-c#LEr$?bI7tY9&;*`j8#ssLrEPf zlgAxt;ld_vi6X%IF@MpP93;fO>M~JttrCo9vLqt4@S17U`D~x?2Wg{CvD^gegG>Z7 z`aUx{2z>fEL#F$!;jj_wORVa9jVSp>9Xhn!T2D5S*8L>6O6gE>A^8p>O3hEQFt5)R z_hXDkUR-x_z+9EU#?>79mNVlY#&r+z1@9}h^}5=knR4!Pv6mV)g{uDgBi^mkRjYs2TNK{)eJk4H%}c^4ZnXz^Qb&(_MAlEPk&%O z+DmX5pd~%KXQkk-eAH_5{UVFwGj5XykI{w9-k3MM&W)7P>CXCN(^_^ZiI?P)7DLfj ze?`lhF@qran-jW>LV8jm;8*{26Lz#2>!TT;DA%}-* z?)(R)SHgrMKEwaEe@|lj3|aej9m^`2M>9O9P^gRi8bVnuX0>aktA?rCmDDZ#6t5T%V}2vYc-k09Gslyc3D>GM z9?mk^O6UII;8^EYFEi4nG6};`H#C@x3capW{JDLG8%_J%aD8rgi@TjfX!i->&pSi( z(KXZdNinvr`S4J)%iCe!peK~)F==AKCZh%$!56{oYrO>$$j%9WV2js%@8= zB5y1Z5z{`_X!pOuWJQW`gjiiP#nm(05zLaiK(__>j+OYuEw4Qfp2yU_WBeM12SMZ1 zI8*+dAPP71N#?mbB;4%TWISO`~ z^1;8u_e-Hun$pPvS#+~k-tBN{d$>j6zWgW&|4QJI$LOKt?xC8G6v+lhzEx|&`f^?h z^!M%u)c^ca_{I*S4N@@){V(uqT284I+H~I!w`Pw2ejevce@GQHpt9rt`t@Jt(M2h& z7Ke!bFL|Z2g-(weAM>LN1V6Yye``csD-lqpiYf|*)K6CR($5u&TYV~YD2%AgfM`XX zdi-Tmwh}p{wTcNzWtEAFHV*{ggWmevEbs(2XgBgO)+Y(HYUgBq>uXK2*&4)oS(w&nH1XP=rSioB7M}$NacI+udSU{B+0`a9;=S@ zBP8m_o5uCsCWMOvj%?-LY|H_{PwGP7_D9HfKB{_Rw@SCnQ_B~fDmCLVm3$p!tW!Vk z<4C|^M5gX~Kb`)%Q>C_Gks2{#vM^oHG>VFGOFI8sem9i0lcUqgC+%$z#PrR~L))O* zx(laKlaoGd?@_54TT`={QShEdr-UDzY!RM6bgxQ{Az!7OwOJ76V6Rh{t46t0zx4_w zt1B>wOfov^F1Q;DMaQk^_lCv`NbX@-W!mya%wnR=dvb>}FX_t($7`fKo{~*H%?Ljm zcKtUGrfAgud=lKM7&%`WK5NXJAO7nWaB3>PU>H60*FPU=;y0-pBZbkn2l0NCD;*UD zr3tURO|DT-RcH;m_#nJuxLI`d{P%Q`K@>AV#9^)M55=hI@B+9IXh}+t0t;wVRZgGrmqorO8Ka{^L&&q5af2SiRFnZba#%W`PGv$TQ$~7{yT2*0(^x2u`2E( z*e~n-d^5I0KbD5yroijIB7q(d7><2kJF3bUtdw3m=rCqAIU2*NXG!H^nHI+gytFnH z`##d``GfV@FX80*jr-Urb@)?W|2>`c=fTH9*-561B>Y(SA0L7=(rbxso;heI_;H+P zI7IZBOia`_*>rp`Up$&kIB-j~2!AYFOZHl7Kf@BH4qhvGjF?lpRZa} z_BT1o&urJ9a5De7RU;gnuz54vEwI!R9*k3av#Cy0uSSh@*ckoVY>E0XLY3W5e{+vv z20R&I8~OAL|J}6oEb79+cA<}-D)}cuNnqqhxmTl?g!CM7hG;2Nw?ElYCkm6MjxZIO zs$OXcfkm0iFWpR$z}`$0hbp9F08#pjaG9FEe}a7R4m?dJN<%fm{_2Qf-K<1x?gHA6 zp|?ysI5VEy$kNXP^KsDQCK+hA64I$m6C=orXCokHwtX(<&$!Aw5fk^3GioZd+_VYs zByC|2G8plC-4FZFfRaztdnd0u#n>or{{QiG7C=#c@xoq~?pnG-N?Jg=yFoxex>1&H zB&EA0l?Ey4Qo2FuKOi6=t#k_rd}r^S`_0HWqYUis`@ZM=>O2p$ptr9^EY=rikkP+& zJ9vI0*s8w8P(Zi{U6}tsXK0x9W&8@7t3QR@6|&M#+xs}{Bt3NA{a%mkLmgG3v1s1( z@RW$n#ri~ycinT7D& zW%XLmBS}&)P^yqOpu@<0F2&<9e81e(+U?u4@owNh_(xQ`xZwY=AS!X?6rxnscII0p z%K{<-kwU&oHqqG;i%iqjOn0rV@HfyeY*%dk1D@Fe-Y)MaLUAvXv zM?zi;BRs)}>~~&`*Q#2_ckY1bMluxT!#6LD3FV0fAjM2oU_QQnM|qH*9Z6Al*e*@g zk|GpSt zD97@&kan6feo5AGkuF`PD1PhO2VG(kT`aa{JDlWwDuJZS4dFL{NYwPbVy~XyihjDB zBVRdC`AfzoomcU2UHLVfV;Pn*r%dzyzMNJ?)l%uSyRU^SoD?NA9+y2$pnP5eV)o#s z!LjL?;{56gBW%4Ayj-uJX%FPoqMmz!pacE#5R6RGW5gb3BXyVR{;mXvLd?9D9Lq`G z9-R%bMpMnPobqkV91qKy82$?|;7}gZ$dAEurS~^T-!7YL7*DBKm(jH0c3=;9zQ{xO?dT=uU>zg!Xp=xlx)-)?wr(KR;3usCOTVWW-GC3 zDs=?B(fjKBiCG6Woo=i8K}+t*I-6Y@Q5lMPOX0o^+?u4<}A*TS9w2TeVL_oyZ>;V^-hOD*o(7_ zq9w)zT}(KoV9w%PcwEprVN-Bl)e z+0Hu;twP;{>m0o0%;hg}lp=w@mM zcBbH2rDbj|kjZ2qx^Fwni*3Y%G?0g+#}|?nON@0tox9ITy|hT$Xbf0w!4|zwULuk1 zh&!F(kfcyd>AO~)`(ud}NpzCtkj}{d{VrT&MqZUN)F-clSXs8T4n^P{TGy^_)$@c} zWv&t0%m4gI>Z|!ABs#Zj1J}$6hv|_B8L@erj{N=*o}sBdsNi%CwHK0V6{*O3=Yi-b}18xFhaLI_c0M1K;U`VBR3QZEmE($iK2Vy&Q0 z;b>q$TL)H{-gh1dwCAeqe-aeOEP#2ClL4cj7gY~@$9~7${-vJ7aR>LdyU3~#1DMqS zUQb&i!xjzDbb6{#JgtoM6O3i_HDrDKn1uAv)^b@WlR#cqckM?rT{O`yXX8{IbrUK#aNJ%`E!q?F+0=Bv6^gY=_jGw#4rTPW^coeC#5(bxNZx zao#(hrB%jh@2-!{|97_2OIIwL{slni|)5?jx zcD_-sw^BCTa40;;iUMZ!I-FlmFjM1{hG*^SrU_7Lu${k;>2!a9tfUS<(FhtVnxF5J@y3ed1GIt%d0FC{&+2u z>b2(F%r>h2>{F{9TRoQyIoe|F_`zFb685Hcv7@z3O&>j`Kd28P3Y>!ATqJm$7XZ$w zfj8CfN}NZ-zVuTa3QqO{m?vtf_N5(H?Vbd0Tsotqk+VRTo7m4R&)^xmz_1&jk=RPd zz3)YILOZG25>;ehY9U3WgK4C)5jYWaA}X%@4c3+MPQlLPL@Ip{=^1wfk1D z46s^=EDQ!xt%&|Ux0w6-kwGYN#T0S>OC*;B2peGgn@2C}y7{LZ5KiF}T-qebPu`Hu z(O#Z=GnRYTmYb&@S=nv`In_Ie>2Y6TG(}^i3FHbGK*)tuhAmW{YA{>Zx6Bc$hNcoZ za-veokiU7}H01X7&t;k0o=}JjLr6N45tl&8I9172vY%ACBjXD zCx6v$*`n;GHcqq5K9gr7%Np60xbDZ-kI!&s4hIX_H&QU(Fw9i2#?56I&(a9XPG!{2 z{^0X}rrf@L50_XVMNa0gR#CmUfGSgJn?jGgL2ntNJ6^7APhvu($1LMJhpa&`+O{I4 z8?H;pG5dF3uSbF3*@j9*1r|A?&G7wSN_?`8d?$s45p=ob#Du3dE3M4%zk@TO_Xe@>+@qs zeoQZ#Mb;HJGW5USiv)3CI%al#w8e3Av!vOX@72z93r(4gd6rwc?&*WggovBqn{<{7 z!Fp0P)fjabUhD$Tcs{(h$jU+Fxc&6^CBgy<;(ydrCyab#Nu95da6& z7Ec!#d>*b&+k~N|4%5o@JVGZ_T!l_l*M~NyV5eCS6*y&H3wEvZ?psmOj4gg?iH?~! z)4O4K*$qK5=HF9~AfDJ^#iX;TRnmVdO~%8R^05>c= zr=JXd9CYk3(9eDSCVCl%QG9anT0%8LC+sH5m=p=E=Z_Fi4)r86gE8hblp!C3#c_1} z131+F^x^LoR@Sq=L(bf^1_=sMC0520Jf30{bxU%30PY+j%d~Yi^fcbyAulL>ww&^l zC4(@RmBfYM?jvTu#V6{ju!E~oP8Xer;A$Oc7gpYTk(&phlV-$QTeM6~UGt}cjGBch z#7WF_{1?-OP2ZeF#uXA2B;S6Zb9d|Yai`g|=z^8MyO(j9i6fA{f(dK5J;O83a{^FVnhaPU^-_@n&hW*j?;Q<|IS~z zi8hJImBxr>X3*F9rT6(qOL<}Jmp*t4EmDCarM${n_#b(KBD#@1S;&bR#NPGT6ni~o zZ%Y1j6{^zph(2$J;$Rjl*yqjrMX%3p3gRj+xBFs@_6DL`;F?>&>=mt({{6k@2rfkV zLw{Ya(6fA78-;-onL;Ww&z}Pq?1EVmrL@2GsMCqEG867;elB*|OL-sfhfA|z5Q@9X ze@c7Z$R1&trHw*Z-xeEdL-eJg%YVqkb03$vN;FI0cP9!Ltf2j*4k z%;AWPlc%w|MWd&Us!(KUq;J^2!k3-6RA6yP6x7zLe0yW#p)90D%rYhaM4Wv&f7Cdg zw_azG3?kW58(R0lR~=0V6e^r=q(Ukh;1`ztgto!+fq}>&VnzK#g8_KVr!TcOOg&Xj zpJer<1(ETIhi86n;lNI2d;k0VItm*qAo(R2tje5LPWX*Rf}yZIn$VF$?}wImyg0KY zpO)%A$t_crp)!j3bf3yYe|$xH;iKyn^yQsph{{+I10_QVW*ne1@NAm3c6HGLp6;Bb z56$+g)T}bh-e$=93q2IgLGp;SF(TC7Q+{`~-<{iSo}|{9;&<4Im{wNKE!@`coLC5a zj96JFcJ3--xYOiU^n|?@ETxnB_+p-LGYuIsm3bAV`=8t0ee*O_2;q)R*@!w?uon%< zE$$~A3`kQs;}a}&|6P!7V1&MZvh5fXOo(ZllciWePKVO8ej>QaRojP+tlWDigS=Q= zw2et^Vg168RHj8b`6-OoWr*3+^ZHkWR9AGXc>r>lh2^{_;Vg?v9aZJj`y7-{MVMat zjf8QNvg2Jso|kxij&2(vwEY4yR{V2Wk-p+b`u#e8o;PrQAY&-~?iXjK)Y=wBeRz2I zKWvA#1u4LxJe`umr)e1;7SPaYJG4&*Ecd-zQi{l5p+xhQDBLI7`u|li5}WLl73|Yi zSrbDC1>G);3k*s1^ikd3+y0hR&;b=VoS&A~zo48lnNg^lwU9hMt=P)2rIuEWp5M(t zl-7_P|DJR~kEF7)NSlrR-irzp+++z_c$G@cCX7@i!ii|-`$UXSq)Epot? zy-n95WZknw$ev{gZ%T$&!UY(=m=Zn-->O7)3`fzgUC>Hy;Rs8 zEql8P1bqFU>Xh7AhW$zRFr}Qx!bWspDu+&7qMox{7fgmY<|KJE?k|49jT{4(E@XQR7u@x zH+$h@GK50N{S!@CbDi@i%ipCUaIR4y)6#7x1Ly-Rg^-*(a3LEp;V(yrX5f{T{vO+{ z6rg@Z&8?oQR2werLWUI0U!%Y#D>HI3&g&2$Ros=6(TJS3%ok#+!J+ZB` zS6C>NYnqWn>;m@k4ELt+*DlF(3cOM&i%F;jJ7;Pz#px}>U~ih)2Q10iGcRJ^ADE3$JyUvmc9Dy%itkpxV1tRHyNov^PwraglwH%7!fs6P+MP({-o2Z`?H7sq zA%u1Fwej=K+lyCmjIq~>bL~>c=G6RYu)n!w-gRG^>7*m-l?nrA*Pr>|D1S#a=p=c^ z6kqC|ko(H-kPttNAj`P;-`i{!7^%}U^cTs5pVdX|{8AyKe^A{I_kMPhMTFJ}6KFA! zR!MU!@xgbfm9v|ZpC-!in` zNPZ#>iz338{7WAQBB}sH-LVG(%M^NkGh^u8TsSs)W))$46Ja3fBmZP=?tR+r!GwA zD_POHz2B7o0mIoIgCX_+$hxq}U3P;tepC72fIkT4ucYP>}ucRlA;< zu=E+n;a?9CB$QBTVqE3{$%|YP<@q^T9RWbLSRT3<<={xglGjm^Tu6KyOsk*3YJIg5 zF>=W|z5FxrVdC}PlF7GxN!)<>YlQ7D6-T>Az;h%u_HA#7qbQeF*#1rb?XPM25%NK{ z&5W03g@Ags&yaxNm2U#t(dDnCln{$wEm4KQ%(09WzJPK1v2phU3ueebwy=MOb1i8F zhO3(~BW;`S(K2;`^`TalTgg_#3#a8%-<40E){f6*^Sa!*U9Fr~Q#s`b6kJ~ERC2|# zJvdLJSa65zBJD}m=^wfWnGZ1E`b4DY!PB~#zY-&0`= zMusm_4dPjJ5gY{SrAl7(;+qfVNkepC+Jxwp4RU+bQw*_Kc3ehbru$}5qzk<8%E;Rs ztcuEb6V2g=cM>Cu@5sk+dYJ{abvcZe@nlxdI;xEd?hLGBvj{VXP##B#IRU?=N%- z9K#5%7tlUoc&hm~)0r2E0pH(b{Pl{>`yyfwZ^3)>j)(;-3AEf+o7>2HE+I>hf8}?p z)R0G{^|uhTJb^quZx;3vzQYrREYY@T*BYt6Far@+y#e}!ObIN$Nb35gkM>DnOCQ3K zpBf8w8ervNFlgIg#xfQeA4!{3MA+(E>&~jsG_7o&*an-oz z=;wC`igE)E6snLiHQxfBqj%LKBP2Lm?tX@4xcaln;}mu3R4odD^SKNOeu2ZnqP9s~ z2+Z~LD*(hx76*z5fVSjEn6{X9+V~EW$k*GxBjaa!V^Q@EiQeoEAHKPXaXf;z-~C2>2U(Fxkh3Jw8gsUj%FMrZy=bs?SS|QfDhR4dc6FXHI5GvvDBv^o&d zK@?h)^sI(M^(T2HAAETgc~R4dKYv7F@lGN4ESI57N8|)+YiwaE#ffYGcAj4Ve+GMH z9;H^bVLepsJ-0-WJ5T1*5Im0+yWu(h?~GcEJ88@qP!xsJkT~Yp5A=EHtf|a?wJUGO ziD^f8NtIm%gVcQ|8>Ju?l#vUn5rKBgLRb-DyJ;nZR*BwY4HoTTZbmujQ^O7f5bjfOlbj_F=RC8=1Mu(9 zG;SN|1uj${+pZU%94$J{;UWdjv`y)OW&9J1Q9KiL6HM2WmP3LNy@PD|PWKyv7i#7=oy;J( z)l;w3D5|f=Qt^U3D}AX=E*$O4kzjdr`O_3-X2Mw;f>2i!x>l(s9BB@ItOUplL-E_a z;)}7TpAkTIJilr^jtB%gPickErpQsl!yoT;4w}YnK}S+-k#wdlkeli2=TGX9#Ey@Q zG#v1WKg?w?t0Xgzy8ZE{VA6qgdda^>wNfXz8|L`^7a(rLs|I$4TYxabs-7IC5c)^d zAmuL^0v5JW-TDxRiQb75MLTIBA6L5Wx91Y2G2oYEj?$d;x8= z^FBu^_#wc%dv$lM*8@_Ga)J{=hy0fea=x;=X`8C;^i5ghm(X4EUqc#x-Al^Je{CV-a^t< zuyu6u6-`YAcewMC9zIAm9l~YCk2;kN92}0J2jSsQSUQ{KAJ;UP}qz z>8JGgheP3A= z^y0$f#_q-qQPMFAjk?*Ddh#_L3zS2bV0TwuawG8mu6!XDjSr!o`Xs5ND3oPFR{paB zd43ZV+O`yY`m!8^{o1u53(tj$gV~~v?%|7_UfA+7^1}~8dX=Wp{i1FESAK_5$>;P@(mBLwwceE=u6bk+ez@pT#_NUJCjpG)r|l9*#s{ z*7QAS^Q+u*o}(-07-24>tdZQmGyY%AV!mgSIXA)^wzC?AAd_ynHFRjTMUbWCW%j3$ zS$x)to{_N|`O!2G&1+M_NyU0!p9A&Og~xQ4aMk-RnV>h2ZV?-dMUH7Yl4lbAD&iaE z%>;IKqvZ=>TsV;nTzttmZ@ZhOoe89i7}$knATR}aTMo2kZ|bmR(KCmf^r0T@=U4a_ z($gE-n<^zUjst1z1A0+8lVlyqGQnYotYKloVt` znK9{^IXls6Rq7@q&oSB?C45ilxnNi$+wOY@2l6jpl8K6nPOYrOgO6#03ZN3j8ca22 zT>*6$@Y-#a1f;Dd{u3worFIVXrMN1;b>>(`J$)Kd=P%L&yzxbu%<3lJjSClrmixoAZzd zJu!@;+y|Mu9u>pws|k7Woc2dW$+bA-L!QEda_LemPaj=vzQp_+C=m%e0b^g}=F%JR zhV9T&ChcSjQUUUyy-WUM(@@$ebxuN`WN~*g zALub??_fwJY(>JHGP(rx(r#7YWGthA4TO@BdFScKKfHNMPEFo^Ii)$#X zIXD(_{l*u^>#}NMb9-f(EQzUbAY%%r)_26^Q;;o~3q@;Gnpm5}3v1O;myRxqMsVfZ zwo=tbK-Nh?LvVB_MfuCS{rbL##i_a|$Pt4KeqQ9#ox+U2)R$FW0B;Q38s3W7NpS@m z<7%+Z5VU6jpX*y-Zjqs(#%~PB=*uOoz#0wc(=l1fS=|s_^PAu-Id8%O3S{Y`QXHbv z?F%Ux+sg?PTODb}BSG`(23$LnHNzK<|Bc{WS*>*mfh)hX7W`6_idlQ`NX**xsd5aVu?F8)aTQ zfb?%9lTU-V1s*_2*Mehh{m%*>WE4bU_GT+MN$`X1@y&v*hvm7Y$4U2algdBge_bc% zyI$(V6u{Z_%(TGFe*xU*DEw6Y-?RI~#6&XCnbhb8Y~*R6($+T0{0*#r z!(P=NeK#=uCq#CqZg2fL#`*L4ZLvB5ba)R02zz49QXs~>yxIm3PRySZK>qcctJU{( z!!7vzm_aK^uG|?EE~!CUB*h`Zp8hB4fPROyiLd6PM8mr&ndy#SL&=u1XZ%j&1fpiV zcLVjV3IQOo*pH! zMdKU^GTm45T(GTR37A19blp2@1D35m3H0-Rg*U?Mro4Wt;qw1#u{9dmJzw=0dJP)u zE+Rp+>eYBS*R{FPdwY95K#93`xnO?Wy>2mErh!n*odAmd%x035B&;qNuw)BC6r&sS z?yZN%vz*-AQy_m?2ZYw5B#A3SqbVt!O?4affjm5FVQB#SB|=b%*>0Qw33d^{@qj{) z4}oOA0CEL|Jz;=?E2g_!999*dX>nOg1ib3aUr}^J00}Emd4CI5Oj#hq`df6YU#Q)a z$@QwvcAOM}j%x#~{@*~OGe)})yqS64z*>VC9a;?#6cn@;9Re+M#OvX%xGbuv!38gi z`~$eu2#WDVocTY->^B2-&3Jsa!N%TGv@-mIm;INdI zvD>4+FyTRqY~aNf0X+a5b|Ks^LXRZv-w0Cm(~@gHV9m~1l27DUA?LV{?0D^!c@)ya z>fW`O&)?*s5qavwOHxu&p5Q;8-~)>r_K5rqI}K(CyK*?qmhbmQ(PE1C68uAKBX`mE zaf(>oLSx;uvka%l=gW50i(=EQhd?N)Ae1`@^s;~vXJ63JKm-UrLqInN7)n#~^N~q` z2iA+-!BTK1C(d@|z`NzOG6a{ubAj$=L;TZBMFLgoEP_>d zBJ0DpY2w<83S^pmkaYyB9AGEBAw53bccyu+eVC9Cz(3psjD4!v`s|9pFMuC@3W_!q zAuk4f5fzNR;-n!!Apo^Kl7E+YE=-M<}XXTkfJ~CD4qtNdYU0@+2Mzu!TnS&t@LSP(HfucY3U?AP)WJONCJ$-7*sO~<{}-9M|M4;2d#KLv0F6a zo`sS>VS|3ujpkml#+AhY4t65M1+if?&Y;E|j>G))E@z?PB!KOdGkL9KdUE1##Bz1| zNAvgf^8XstztITL8D+)^sHmI6;s#@PlrdyDNzWnVucF$6x`~ZToP^?lYY(sWr#F=! zNFY0}&A~_8VKs;a;EWb)U4egl@q@G*j=-m;1`HV{$A2Y3N`-*LkAmClnCl!|>g*3d zpa)>JJ-`V0j_!U>>7uf#mXTOw0t5`5r<%gQP>vy7Mhz$*|821jd&%Th1a2BdcXWt~ z175vhgN@2GKf)0?y!8{v4M8aRwXnAKh$Sc+FgTOVfH89Q`V6r=>`GpX0Ponp!B;zl zQM5&XkZ(koyO9L&*AXAQmmF7xL8a{T>2Iyz9P#^$S%m^?!0v-Ut_%}n{sDnZ9K!XB zip=}*=9~SHXSL7KN(_~A8`ATio-d~TPQb!f{?wql2k_}Z*%V>C|H^ddv0u}gst`vy zoj~>63ap8L5i3(SkYEx(5C$AA3gsLi8$JT2K{+cvITw#U%68qh(#Lcwd9pu5hdO*; zbg@dpjDt}!s%MEjl*isZr*kL@ny=)PE<%E^J2@jM)!4k7pvtsmGUu)T+)Uz96HK+g zwi@Q;;xg27ct>B7Bxh=iWBQ$Ev-s%)W;xFzG-v0V(s|vGg$Hrn5$nTW zP{cBYCPUT%?VS}D7Z(y@!W{)XH>-Q*I{+km{UI1&%?H5MZBKPWnCw!I1CIl2ulEDi z?moPi?E->7f-R192 zzrV6+hK4$c?>vBp(Qh|0*QJXBOlOq?Th?ioye!5MV6)0Sm|<>bF|UqC>xvaXYx1;` z4@1%Ly{9%N^YGu@)}G3`n)^wWK8Z|KV05Cqc)+-2F#cvo^7(nr1!ZKwGAf_uk!YD? z;pcArnmgmVFYOd!-bVxT@a4LzX2FrYP9$7^WIc*Ks+9+|Fad4{e) zI4+!NE3@Erd>>4wBhF)G!XhmK0 zCJhX{7p@DI!f+E%H0F%FUBc31EeFYt=r_V?8T7~SF_r(K9r;mY$Hjr%~AN+4A@IT_5~*vdtc4 z9~cD@q1Qi&oq1=6VSCy5aA@=m3f17mZ(C^*%hc2th%edKu}jmrCh6+sI`u8kFHyGo+aRVA`RFP;2cekv4Aw!8Wt*Cm6E|5+Q~t z{y_w1RY-xfPnEMS3CG+RjO9P(LN-RWW>Czt;=`=m$Wfl*-N9lg7`<8cGeFa!b|;Sv z9i|JWMwo*3fFv*!6`NGv0(0Rlo?Tr`PF#j{@e(W+hnW0|? z!$+XJvMTEsVI6JN5E9z1Dyeu;&3H2tVut_x+CztcUM&^*Vs(2{(&xzJ*ze7oH|r60 zfL6S&cKpr?{JrYYMfAF8D0{@0bqArwxkb1Qd_k^(uq6O7J6UMqZK5$oe>B;13`L=1OY6D ze!t!P=a#!`XMKm#q>LdMNcIB;2FNg!WGCJ=+Oa0p(B+M)KW`mHT>R+TD7lyvki&mG z@xXw-F6M)K;tL>I@u7PC=k_g&QfU^4B<;0F-FfcR*VFE4VQbon+qu?h!|8XR>7qS? z&rtq!go+WZk+i{xCejgFK(KQ6S{Aw^qa@o7OE-o%5$K>WK#Q{4>Nv514P?@Fn8rt5 zfCgQZdGz$gDyfKBq{u-gWUp+DTx>ItC6+*b8c9@L_`3wz$^s*?4svN9{OZnQm@o@j z2-)=L7RsP-D~>k8til0(LqzP{Xp0%TK9L5pIX-3|^VRR`JO}?;(nd?J^GY$40)I;IqI-0x+5 zTu8Yvk#qV8>lPShc6@fgF3TZP4sZ>3}6vH6S zXoS*3)Q98Gd9_+;z(8gjNDd13MX@5F4L>+LIy$C){D_&D&;$bmZ}eE_P6xoE#fsSe zFRqqdq#7umXr7o1F!Ry(nvQUdL)~U=imK?Wq{F%7*ipy%UuA9wR-v|z#h0!|W9Kcl zaL#%rykwVGfg0u{W*mJfz~YP;j_LKQ1D*5dJ~5s4O6KeMeQf@e6-t42PRPV z64%Ko@OJ;gS;_R~e`ljBJ?vEVhD2prhbmrZU?G_l_TXBdNpiTLuiNKS3#qs@%k$}n z>08R^$Q#-r-QRbPkOx>$qA@?geZ9q?YIJdR#gLMRXC#J&`ZOw*0~;AVC;^5fpHTX= zZ;|Qop}naa9#xh(2}mIHXk*#1_*ycpgryc>K*5c zfC_3p@=;V$zw0(4vWt^-InQ1^o)wm`_4YTeqkQt4HiFyY>Xl+E7@RJ;7_tV zVVPqQV}}2N?()}luih}!)pLXqeDxgUh=nyZ37K#D{(+ducT!9Acgg>*EISuNR4%^j zcZl>8_@pUC(LRTsq6e?|JsYh@d-@JNghtHU0)Ut=z&8Y~t@>(QJXD*s6G=!&K&W4Z zd5R_g*|?R31>~#IcA@;uDqJ8so54)=JGZ4l*o=udJ%wgQMoIPC-wl>)OkXbU>50`; z#}i-9`QQdEhtksz)hH2~j4F+XR&E3~!H1y*$ao%oTcinJSr^OS$_Y z-v{3BQGF|F?Z&@by0ToDs99{bCeFET=`pY`c9B*pwCHPbguK`lTSbyi*2Q}Lia`D* zTC4RtD2U~OoNEibMP@CDOPEpfYkeKZtc(lbX=Gt+b zy98r)MUHu7>0ERb{A`iqO7iaE?g#hUKc{gHQYVjPoAKqA7ZA%+kM(d+DC*tfMCU~t zK6WP@cwJ`*{6T>r_W8Q-R@DKN>cX4tcyWqHhKK2r#LqBneQrTizrHM~J|`TMnV9hW zLso?dUkFM;g+_wZhkzrI+p}?=sGx^J{J?0vmoJea{{H?a>+kR*AAYU*ZYE5*<>cgy zpiBLgITk!Vy$FhrS^20$>lcpD{2@cW<%tl3mJ5>+xaEx6Tp1wiKkPpac0b;A)1Bcv z!z5LL+=QM*ZShQ@Jo(S%0b2+e_GH5eB`li}eT?WTIx$}Y>r$e~wppo(Ge9rWYs2vQ z?!?^Cls2B#Ad$et9ad9pY2Bg!9x6qdb1^8IfFuuD5Z#jM-INW~DAn>6DX28u-l0#q z)gjBh3nVA!EYPkK*KuSQ21C%c@3EoURAt+Xko2n_v4%~+;bulC9ojGFU%4IB4{MHO z0pmpVCL z-CUYT!QNCMbO4MpGqaL>FKb7^V>T_j=xnrfXvnPXJpOCiE$VSg2ti<#bFwh{}!Xw3}38cm|FM^(xx(m zUt9_0E5ZPE{T5`{h?rQ_08uX7(|Df2ntek`g($?xT?zryd@;zW3xF_YEvx%A7Z=yX z_vdRRE?qY)KRW}i#r$h?qVFBy@X~*4i9YYULBRiY_oaEDASf>4I|i4W$)_G#>s9nK zp=Oiv)0J-A)mu&J&V&PvY4LRW6*`|uFC-=t`32MKq>z;H{dd1~bmYvj3?n1dTS9_f zp{r;qR{nlgv7krr+PY#B{!GIl@u2vG)9sQM_%C35cpi_Q2a#x&^&;BH+D`5?BRIzgUET>|!I1 zN3uac)wv@!G&$AG&SX@|tv3!mOxR68Z5DHdzi2HWnt;G3V9qqEv<(H1v%Wr>UizZ` zm02@2vUU31J+DMs(sXLzF`q$v0zzJ9t>3q)LeKycGUe5wY-iF%J1L5gEC1> zdHt*aE_mu$6M=23$m?abp$tOe2CC~1ut>}Gw2%LoCqRY9A5qRrWd_K1T7*5eQ*Xw| zK0o43ZF||47}%;HJ)^82<3U`Ni9~>?A~jW3Vj+qmt{_EAxV`2l^f2!5WETNtjXhxO zlV%?T0ap})>Z_S6giAw1;|sFCIuNvtG{l3tdI2c4O4k|w%Xw4VOROaxIzMl4adELO zM|Vpg3B-XjAaDnxwAMgO@fTG6gg1h?Kp`7pdP_I`hsYNy5e|PY9*{C8XBt$LP8oxoyCE`X3yo+421f zvz5J+7q{3U@u2nljOzfZQl_Km-6qzsk^V~IYPxc2I3lS+d|dUw^?1^`Y{;CZB7U`K zEex(UwA&sbDjNj__C3(4L0xV+1=@Ol`iLY@=c5DO#Yd7`)BFlI;C!}P34XkHc=P5R zeEIL8OE7kU;g;A^R09KDYgfq#DZ{UUM6!*#cFgazt?wl%)|* z9b$zbkPZgQ$U#HM%o-D(ZEz4MXU|}BxAokat_Iy58n!qYP5UFt+ozyoQmDXn*c4e7Q?1XHvn)%!wyG8rDE)K3ER z>;gjRoi5g+?aOdx34?arB_M(f7)8s;9%3+knT`%5lHVLoKRU3V*^}?BJ_>b+8f&N2 zhLTpvGMXmoeT%HbNPaSXp2d!;ymA$@9>$C*uO|)1v5ukg8uBu7m#7I zuZ6oElwb`e1zj+KDdE7jmX@nhrgzFkyz0a^CeDU!-)MhE*1g>(7v^Ca{|{C0`Ew?7 zHVaT@(e&5IL{d$e+?C*K;T*?PQ(*V(XFKC(gn0J5PH46qmM)w{K%K_VcIZbN0$vx_ zIW1dKo2(bVrsN!#70QiZR^ink2GM6Yfek;!B(BM#wI>IHyzF06Oj=}1@Hif8zuY9HiuK$kcxTpXBvGOR@=kL6#C8@2!Nk*7! zUL?GA(&&SW+6MHA>Zf8p_iSpk)J6X4rNcv9DqdmU5d6Tu{n{&k_#h)Y?zA4Tq+to% z)WZEb6Fa52LGU1--F)?VQP@Tgq%VFg$Y91^ALe3q^Om;E(${$@r>wV$69*Sh7N(Gu z86nSzq(i_9iURtRPlzz1YiC=`*nDd=K;$BgA>^_e|LH}!6(~$iyKsXxJ33#O77?Ze zJJc|a(aoEn`~^$15bYJ4KyxHyJd6-B6TMVC9o_g&hKde~#B3C9;a&W=jwc<R zcB}jK5X(cNS-jEOzqyYpF<~L{jKAtlzL~y?8x&WJ`~I=V4aJ^*yydVW+Y_8k(4>#? zhZN*lWRq@|&@bxtUB!a|RU{gup3>?o2lb7E6zL4Qtpsh`;ZEjo`Z9LqvHU%eHf)-o zqGypPxdZgkZZQ7)EI|(r8#sac;B-ZGwM-HR12=m`RgBDR9_$@Gq=8x107HINCH3}3 z28zMRrZfFBMM>=($)Pe)Pc&4ogNW!*i7_87K|sYi@#*KUsntLx?7Y3Ch}HCmnBPC^bmi+2vA%vC)VDX{AP1(J7P%Q`_BEC~4H)QQA}8za~YV7x5vL z%;*MCLX%@dF}LAJL#WBJw@n#@_V1z0qW_Pks}5?jd%Cy=iUfCecPs90#VKCgofdZ| zIK?G66fYDgQrz94xKk)ai+oSt`F=C}6*8IRKKJh3J$v>XYKh`PESXRlDaj7*$jA}Y z@htNGKOH}b8drvnZ;@==)#x$-0)yc(!xSiBqRh~ z^*a^O;IOb|G-l?~hc5D*6b8L?bO%zZ;%RhsQB}2*RR)Vrvyb#R6YmcMzWXV#;R7P4 z!2nbF`O9|;Ef5Khq4UW^Giz4em+U)iZewk^eooOax!d7B?}0_&Kmew9!!}LR8x7e4 z*d$R8ge!GwOrIw#%v`a6I@l<3BBJ5lc*AF>HBMCyYdI!a$t80_B(ys0GT;?ZzlYZc zC?wLH@;ze@r|MMmEKhqpVHv;`dgW1+-IRP8CxJ)hzp-ACgH4S6f*9>a zkT!fpGsrMg;s@v*(*m9_CML*sPUodZ<6@CnZ$AnDGowB($}0Nj>HL;DG2Jc)NWuF- zn3=0P0iU^OBnKzIaDw2|wDdC~Zg0JT(2fPI4QFOeg@u={iy(b2#`#C;k20bB$2}Sfq0;6xz(3=Q+pb)P?M!h$ zaUpjh_F$pD^Rf7wVqd`1H6sHbaIv9DS1JN7LSVC`juf`E-~k0pGA*i?mseu&8lo;x zTKqF51QG%#r`p4%`K_%az|F!82)kJTr$$CoX1rEn4S0j&+8}Um$HJJ%%OeAqcO=lg zEf1Z@6M$E6{5;Mcoc^@ddfvk1R%Nh%G=wV){uM-zN>_+1p(+1HsZV5DTfCxAf2E`W zu9Um7g%b<^K5+PLe56+~tf|I&_cDKkojxZkE=-8E_MNk#4EtwU=ZdxKloTtXM$z!P zI!wmKT?N)y>Z(MO$W2)IDwU#2tz++K2P=7my=#6eR0Ueg=mGFuxVrgi9_5J+qbKq# z9=*HCY03LO!GDaX-Zr4^;a2||CH}PiMO;p$hxPswMDQmqO6XE~g4cWGb;u_JIpFpO z7_Bx_`3V4-K{~^l1EUr2#r{`A0X;)WRn-J!$iI!|b^4AL2X(waoC4sSY~Fgblm6RE zKnF}mSJxcC{{WpPzzI$Y=ziW_Y{Z?M*aLt^z=HF?&tS#xUvKwFQ7addY^|yyC$uPb zZhaG9G`jB&gaHj*yi|JG8Yf`Cy{GPEQ%olQ{JW~;%M@cmDOcFw^X$qFimvw_VNwQUN;6L^B1c#`|$pe{;y@Nw?T}>#UV~B>- zm`(cV|956)Cg!%4|7@cP`5C?oknt)s(|5K`q|g!8(xt{Cj6_t#FC2y-CriLKO<#x- zXjNpV!Hl@bT|d4SBd__$R?Tt^h9aUzZw))LUIBf0M4(M4Ksq@TjRj|rK^0qk$EXNc z7N*jUz=J`oSFePwIlOi_fcfB|P8A^0U(EO~XJlq!nORyw?&k}j4gU#r+ksQ=aX2Xp z>`81aWm=pp0T=SBs;cSD&5N=>Kpp^OV~G8n)3dWXA$hH=Xjy-j3pqJVD3MXC_QpGi78E>dNIMHcG^LoMG1 zBg%91Y<@z9RWi>>Oy>JM7PbmT+z7Q>dFzxBv{_e==Owcn4qx9Y1jEw=uQfou>@PqJ zRU$ld8I%}vZgw_ME&HJ}Y=~!n${_F4SrYxvmEHIGW@)IU&YTcH*CrBRoP~{(-gtRQ zjbnfI$+v-t)k2%&6{C;j$OEK88d*yk8XAs7`v^$}4}e^qu&zz>ZNU&gR?yJa4tsoj zr0&99%de`c0N@{>!vo-J)6>&1N=!hQoLN|i03;iz$dhQYRV?lED?qC>jh0~6xKpM+Xr@%TN?LLXBG5UD3&2TIsyQImStLDg&5B6= zHH}KkLPkkVu5ywPT}do@X)C;Qln$9M@;W3YR#umHGO_fu;J^jgLUiu|i}YiF8aUMQ zcB|#t6+rA8|tvP2OS~MRs%!v5-_6^XYwaa1Xgr-q{p1|Mm1(?KHf+xaA z$jDrRg3*ep^uYZAEG`EI0g!dM(u{F?nhXKnZ6~LvT@6t6g2d;)ca}f{N&}3TVQqdg zUun9ZS9h9UxepvUy@(-Rb*P;%F@sV{8|%O8r@Q;5`Dh7879541x~D9R2urN=RUHNH zZ}kkeezC@)-TGMD-$TJxTzZXA>RoL5l!)3Av2oAm7o<@ zgu%5qkPwEv#79`sCoP&dOmBE$@wKF;)Hup`ZGaS%;Hp?9v3};jgxf+ zslj&}O-{?VFn>Y>sD^d17UZIp#F*_ZzT5=rzZ2ljHubzalNsvuHWX*FNFV*rZS^X& z6ma$`Y-+j&qI*X_M0H1YTv8&2OP*C9k&xy2Lwz=7&WEDVX4OX402MVOBN70wNdbD+ zHwGPvu$*g7l6`|VpH`r1d9eT}Zw?^m*N0Q`)G4mHUsZ7*K5i<{U|?eUgR-606w0(T z)`dLpZi+gfNC^oE0_04!w!nbquog%_PE>I&FqhUzlyJAZbS>`Zi2z!2puo0eUTv`34 z;0nTzpdK)G5#74YzKs^42M;{oMZU?vSO&LlA{zaI#kR(hM2tqz)Jd+2Ez%GShX3|{ zpcnq{acN|t__(tj^F%6A4_o^xnig+H2=Jr2et zEiGw)m{rKAloEui+@_ktJHpcp2D~fV%8^aP#42j4`sORO--{Z!5D*pHtQ6bPCTcHrcRgThlJ`#G-pAJxf9l~^%zC2ZtPnz}?`_^FXy)o^!i`g7*m zB-C5JUu{Fr?hOi|sIjEcjA65c2$C|?m1ft~uW?or>oiqdr}~zUe~$Nn37gWjHW!Me zM}Rc9xc#ZvZBApHhA~qtblNZjVEG%3ARk&W#9Z(i@6AYp4j!ei{ zmVFFHw`5mf5=Mce_oVJj@(t$2PO#%*K0N!)s;iGqfF^}tn-fa75pggfGF_lcRh1si zaGFw4A-7hyRD_18B5qGaOng9@S0^J%B^?nN`G{5#+s~<345Nxy#2h*&5*v9pk1GR< zUsl*^LjKWT-RW0|1l$=MEm!1bVjkljl7!u^9dHGfV@sLF_h{MugDrOv6@m8aTh^qQ zL#PuWog+3pJBajZ?9z~K@hGFg8HB}!4wE5lDIvGyRnN_yI^t{j?th z5|W0I(WzY?sJ{ouS^sr80rDB%d0YW(vvHJ5KcWIqG8~;;$Tb$j&6TUm1FQCyfO?HEYv*t4*=~Z zI&8GIw)SUKT0)ZoUH5#}ZPZpe`J*2|TqSb!>qtM-*La+;VZdmJ^?hvQNhRODk^D0lK zT5}V5-u;n?8~6vFtK^d&-vd$Q(i-v=Y$)_vK;6KLoD;f2FD}RouK_eXg|$c^4PWFf z-6qMYJ`whiUnLbQU?s&=)W%3J(9SQ>rLYhlQ7{4&NHtc`+gI}wO@<=*vgE%aGeNFA z^P_aOHAb=F8DS=Z!BT+~fBLYPA#Nx#2zaQmiGpN{v_h+Zyeg^&Gvc@&atm893Z+aq z#tsbY8WMIEN7{KX{b8|JPJlol4ZH%(^uhujEFBEMJVp}&z|)~~zJ>MmC+d$F!JCJ? zvzMxw={5zp!GdKLdImW`6cPu>s`HORC#}jK3tzs5HVCQ#Uj|Qro!ph1KE}-A2 zvBX~pfMx*A8rUOYL)167w-!rvCecH?Sv##{S4|cm%4|{zAZukF+Zi$cAsg0BjyCmM%;fkG!zb4YT}_8gvlP^y$WZNr5ho875^S zCdl(`94{2O0fC%2I~$SEq@1(u3hg7#2fvw9D% z*Q8mkBrRVK#V zAhE!o+n1&}cm?fpwFDSo)|~W3MXN!xl!7%qx=9S*5-`rp&FV1rdnF{9o0Wi9iKps% z7x=z!rNmt#Q3~Mt5GB)MKZM5n8cPOzKo<6AVjYX59ZAMRtO8omg%q*HQkpsnjq!-+ zYSLR5K6tBsXJx-3RBtCgxkm}55pA8m?%n*S_VSHur2s;vcU*3Wm;vyJ*7h$yN)rWc{$d5~rl$@|m99)(AUf2T*Z8OQ?sU_?55!(B( zt1&O0^fYMUB()@R2fqty@hJljYkiJ=gLZ zFqx`>H*os;ZrTeA2B4fXuo@xlOhyRVGzH}&jUXySNWtc9Y+#uLgUgnINEK}X(U{K( zw;U2e{;(rL20?|BmoO`61CH3P6N}WC4gmT7ErFLmYq4G(w)(9x8>h+RMTC|e#(5K1 zA-yr({El?KS~EQ>a^lo9+#Ed3*^X-8i4$qw*Q}Bi84+67&*{16g_~g~A{ccK!{qgf zl=f8x=S{MxKhu*wcw+T7(GE+&!V#k0=0;5QL%AQg)|pwJqT8jBl?E@a*IkvuG6zW`){!25gA-;+S#gec@Ap3?iz#&` zD#b$fuyh4_^A1Wy99e)@4zvXO8=Y*c(16+7uRZ@@FUC~KS{BMf*;N1;Lo|{9phrNf z7%Q_1T=&3y5h;)4r?ZJ@49i#X02gwGA>D$2Aobd=@oxKO{}k&nLQBgP!5TOYGh|nH z41}$4W4J&!QV13(YaoX0FOC8VM|dWdGaOZjM6LwRPvYo_6Fg)Z7b#OZhfJH9c$;4> zRg-_^**rSRJIc%X8p|}r3eQocn&zrR+}JqXPk1~J=>(*lz%Tjp1lnvs)BI{mm6j5gFO_H|6O8XkIM%LFP2qfr6)I@K>tfo{ zSX!k#md)zr?aja84Wx<>rgovz`Y5Uh0I7}JS&yhK0!vVm{(=#JCq`>dC>X+!$uwnm zx+wR9v8{UEH3dbDg}+hE&kdlY^tF@viCA(pDmk+%Rn)VbLX(v-05o_B2^AnU^a`%- z%!&)JjEZ?$R15txNpfO_q>>O`t?tr?4u`AaF*u>9s_67XKQN$j8TF>o<;iWfH6%|d zrBU08+d!+9#rNSrU~YC+$-#(#EN?^fVR%d19i%353MVZ5_e%{Z+Y!BkYC61z3zJ!8 zffCN#HSj7@f;J7{|55J|C^e?9&vSInL>@nk~cW_rj@H z(#{u8uxPDLsS`FOI1Jd5qlYXgS`cI=$ z@fGw;d;^t(Gdj)>OUv>coPqdp*6w9)gX@jrQbU|iPx4H0D$J^Ng8i3AU=%W{a>A8Y z!4l+Oh_hB}dfGW;)DFSUwvtCpkNN{;<|yN9DIGExh*iYSt}51IyWS<&{4CehVR!B{ z%0PQee&J=hNZP$U!WltkPfm~9a%x1WduTXX)b~*|)yLN~J5tjvFTt{J8bSiGXs)9GBPMaB@2|sT9z+FW}C0VK503hlB)NWKsrR1Nbx{&#p zuRASV_Tk3Kj`LU8Y`O<$IVX4+v{8;}lp~K-vz-SomjSR=jcmY5!##0>D8qDd8 z2V;9SXkY8XPTPT`Yj!T1zQkt^!uKrI63^BC>$FCHE6(ZIzN|BA0$cZ_Af74>J%yG| zR4-jaeZqVW#Xt+Inw5>9xO5TU0jW&O&cMX1?;5JXYQad9O4@v#ORJqRdUIvp) zi-zIxaS*PycIl^3U^v&F7I>T{iV%AgRZfn0UEL+sMJ{In*+gQkBx2qV9Fhhpk8_QK z_J=mM!Pdmlvs9SBo`vX^236W1PRG!a>da3fSi<&i5^IHPpGd`2OLP z)q3@t5OB3>G9{HF9yTx66xhUIthVuXwa*$j{PHc)V_aHEC(L@vCFGTB(Bn{*)nYcj z-uq=(=#Vm48E$BSDUGgUW*}~=3&!t0Dtc0HdT})o3$w#jv7Fb?NXr%uB;DWYsnRqm zT3a)F*h0PP!qioz={%m!Qdv#oH=~FJDHV*U@DIHIVXACiMQ!kzPxYIT5)qRMLu2n+ zW6ihS`9?YH&GF2*Sa2?u7y4R9hy@tJ$aaZo(3|{aMFJ49yij}P~DMp1rUEx3& zxzN`w(@rY(JL1$Wj8uhNfeFjzQf-Cb60j}j;*ymAz`&wcr-Gstih!zEb%4;=cf`VS zfG+D*%q%}8C2Qw^T`FrcStpQd!k;4a&HRQjkir^cr}UVZ%^-C%ACytqP~0ZdaKxR; z;&wTWJ-OKfeohGRfFn3X^u8nZad zUEfn%rDUHI>0mI!Eyw98&g&m_Qfh6W`5g#IjpssPtyra6l@-CY{x1=HJK+Zekr`UW zr^2mH+TNt&oCc-E)D!>CwA$>##zap~&q3)Qwf}y4$A(@yb!D9ueYM9$bO%DEbL80} zn}@g9O_gS7bFe?SiCh}jjKs6Gd>E=aYW-%&D?69DIEK5!&fV5EOKXkg=nFj|*+=~G zKyG-BqNXNz6NS+^5w~)3a~p34HUnHXR%LE=#cMdaN@kVV!tHH4G@|qn!_o{oK=uAc zL9>E!fm+jT6wuEdPiYs{Vp&M#)(oz66)%2{y#tgTek%d`}LOCiXH6EK4axY1j#{A)Gf+U#57>> z@OEl!adlT9s?HgxR%~n?ZvNRlu1b<4(S*UO{f2$B@RT@J+fFWt)SS5ud5Ecqgnl}N zO)N4him)K7r-QwFq>0OuDh=kOCv1L=kCOHaAq?y0~f1F2FFOED1oXl)qg`Xb+kq(+AVxpo$Ma8#zlpEJd9scYVXVd)jnBoKDe@xMu5QX);%xsWcjWcJq!vqth zftwzPN=sf$;3pke;MsmGir*!KP0>)fq-w&0qw-8I8mM+l0J&(V(@sszlKEU$-!fIw zY}+%bJ11-U6@lUM51kUVac~+vKlU}&*#20y5PK)zuT=UwNLzJNODIT8R><9iVCrV< zSw&5a8#lhk=$kiNd7ihSR5A{&!WWH@wdq_FBf8k{OQW&q?>tJHJ`#xA*G< z;ioQ^ zRkBc>0Y){+s<4R#o<2@e+M=(!umI5!gn`ijfS?r$XV(da=3DmRB`2(7#oxYtzPPw3 z#WVtrGCsW`+(N67@(OkXNM_W3+^VRrdD_=}wQMqXL&3Kef2CSnYU)bV{ee3yDvBsK z5N>R~L7TtTX=Zjcgef+jY<=+ccGN08EKy>u+k!7wdp#r9e{s=+%bDN4Cj{cgH**TY)L1AqBYHT})GI5{Z+ z179nEsOBbasE%L&j1Hw(+?<%R0m2sT8=!oxr4Y$5hs4ok`J-@QBV=2V}cn;fk11?GIwU z-`3hZnBMOwS=9m><#H|{g-N%+6X*gM_8X-e^rJ z0s4!$622I;S=1|{_>xH}2|VLOY{l4J_{P|`^UT3UMq-9v8PBgP$@y}GzPUxHOs=uV zO!x3&Ldt|*M?jP4+P3Khoe31w;R2Cs~YJJb%o00`B zED@6(<;Plm!pJE8-3DnKHBNFAT#o#6aMI1R&7VGA5vxwBStXm%cl=e0p&{qpLdUL7 zh1sS}guavF%MP$u3dv^2ijeSS;`i=}q!WG+3+5>feV5m#qGbRyPi^@B;@!#PjBeOH)p@%17FRQH$<5Fg@Ch_zsYfvRy zx|!zv!UjJEwk=-Ve&?YdvC>L+E&Ulioi5u!n`8G@>LKMMV%&aTwJLof0JcDDUR^7g z7g#9{tXTVPog;IH$vjEyNB4x4by>7z3ia$Rh(l#2EwE&ERs_4?`< zyN}c;IKfb9e#_Yn>n4A%j5+lt13rx(77Wq>7gqi49J{JZALDu6DcD4k+jyUFMnDwX z9W|Mi0GaDX-dQI2vDs;i@4Si=AyLuoja(tjV2Vvgkfw9CaJ~OnWxFl$u|rYd{M3m* zpK8hX@8Vbez^eV!-%aR^@C;aZQV!ned6U$1E@s+RO>Rm=x+^x62cbR1&T3al)s0Ox z+XkX4I5z?!&A)?QC4GWlOTM75uIet6C-n`-1h-ThsmsKU+b&GXCGumhxkaOb!@H6$ zzKN6>D#N8${M92|6_B{(T937-wFe_#FoDiE*9xcJ+YDu$;m>Cuwm zR2}hM@7fu=ZJ&mDC+q>2g$MMA9bVOb-icBeJ7D|a&`UWiQgnyEK{d*KnjF1feFKlE(se*~}^iDfz=zvWum&vlMgAsB@JNXoo8EQgf zI}%S5w(63rJMyp%8T3tH*gNBJwrM`>YZ4S0iHu?mgDf^Nm5b>f1zug%np@h1F$fu? z=tNYZMM2%`uYIxZ*dMY<0T9iIQz~#Qb`iyU-Z=Sj^mM#p`*HyWpmV#n-x87MMfg>d zORLJCR?!l4eoZv|VGOi=y@Ck+wVkI~alN2pqN`(I5+S!9LPW?x%BVCqLh6h7Sk!ZN zCds=YvBV3qjGbxF1oi)DRq>oFK75K3{Jp2gEj8wwa!joC8zT@HLm1#&W z-xEBFj7V#GD>=<4y0km5y~M+(U0O0A<4-R|kp9A@c`gcxTd#kRR~UU9JeNDvO)c|3 zI9;}^2tQVtqfH&5U0A^d_8w&~S6FKgjaUOwo3+zIskd)!H!+YCCtN4WzHd|hVHobrB z^lwt1{W^Lor$H1YpPUZ!Lb~8hMeklRI(h2XMp}r}jP+rYi0{u$WWU>X6BY2Pt%(QX zp{YB1wr6D6w?IXCqa1XOj;ze@JG$d)Ni?p<7sobcW^_QMRaDzWR{F=oJfP9$r3x|2 zde`0{ox_gGy=hu>>iQr-q57rHpV##dvluB)D%1Hn#e_{!1ie z3Rb%J@E3ByFBjKuFM40t^k77S3t&F=`d=Wf+BbhgyXSHxz9i(tO|GSYC1BtA`h<5W za8K1xekI@ZjD5LJC&-X^vP^g*L#sJrgWr+fNa)e($jG&OrRk%(xKZ%9M_Yw`s;ESl z*>8ypX7I5fBnEJA1Nvhfq=_@j-G|KCweQ3e&Dr^r4MB%t@d+4!CoXf|hQn~Pr{*vL zCkVp?MH~+jt=%Fs@yS3`6Ak4{ns2^L^h23+Fyh4e!rPKBc{z8CAM2f~PldBpA_2V? z+C4^~R)13tOM#IVnhBr8c-^-12KPJKy|?Z+?cmp<@n0AyqYu5R54BvU_E6OlU+l|l zGjVoZ=h=;W=t*4g8$_jHi@U$4y{8sth?nXiaEmBv$S>-y(qGZ;O*y}_QG)9c-ePnz zK+o%8g9x#di5La9Q0E&-NJLE!VFeIQMqS`u1+v7kYJw&-^Ks=p98Z`OUoiCM+cQTE zxA|5;l2t{6!}JR!hV4l?nn@OS<=y_gne0kPCGekgf3(guxA~YI2@hC=#KfRtk_!({ zZ!^gyj{lyYoBDFDw?_C@R8F}=CXNe*$B1ad19?j9g=5|A?@$BiO+OZ_d5nl@LWR=A zp5kxf(Nu6b4j<&M!rkO$j8_$yaKQa{hDPFWE{8%^ucEnwGTo&&z;hR`@371Jd;W$U zZ^8IyRH&BR_e*5mXENhig6<_apCjp(f7#5}f-eiuB;mxvZ@SGM^Z0I`k$VWzNR-}E zh9Z=fVt0!8Hh!00;Rx;x=4Ia(YOtNxggiaIe;aCdtUFyIRLjIp+L|@vFw}Tq$aWK5 zqLFk`&DE%cml|9b;%s}A6Xtwk)!20;MR|2;oHWxPg{AMSMU(m2+}k^&E)kqpskXZ2 z2oy*+v`hifb+=%qu?n3>=898eYDK2cx=Da6=Z%<}SgeBwFOtg~@A280iaQ0J!k3SQ zi0Z$iETN&+ zN;U42(YQ|%%oH36oY#Sf?w0yc^9n(yfN<;?9`FGj+QvH`7RL%W3gOd|M}ZAG&C#qQ zhP&gRM?d|b1}dB-6NgyJ1-&HuT4fZ;(bj1>K>F&!WIV%>w>nIv_4DGbDbk&|D!|&R zbrNg7t1oV8$x|7m%~PFQOYh_L6g_4r8!yGsYE6X3$U^cXz)<0w;^JFgrP%p>d}?iQr7nUQ-1ukdp1lWBfit_mNp!mCQ z_j|EkYr1Aos5y>eb1Y)5SJ>z@%%gs zw5dJxRW+4h)3SAouRuQ2_ZtDah;M-qW|FEC#Pj9glyLR0@@PNJ9`tr*GXvUGo-kPl z&ZyjmNcrQa@+3>anYz;JKmIk6Jdw5c zI0{B%-_=IEy&y4vjwEP!Y|flsNwM~Qor@=5kDO_ZX( z;IG-mw?L(FQ)aX6{3OmsJKQ6pQ~Rf4@Z4~(GUsW2tf!W^&1A_M*xJBP20CGTfHDwn zu_v6lmDSz{pi)&!l({S{J!SAXx_`cx?vVb-qLD_dneyV}%Jby&(@B?L?M8JSOPc2L z{@-(^D&)~nYwd>lYl0pH*VlpRD|ql%JM=DW=xCjANFe6T&m^r~L1ExA&sJ$q;=yR+ zM!7KIc^J{l`)~>0gVe=uXmfIH%)BVNy2w1CN(%jQ{@soAV#vqslw*a8;eQUIAy&QN z5czX4>O@#xy|mW11`gqrx*2p_pajRkAFZCNMr@?=vyhuH$oQmMqsJjn);qM4T~_Gz z-116bO`;lTtf$$N*9FO4os-8yoGmzSHtaVI*AJD!)cjE=XHksc8cAFGB7DE9yLNn~ zG8}Df?Gck(gx>A`C>J6MiY;zIL4`^kduHXo?qN_n0LNkjw3OBx1D_vL6V6`AFJGAp ze!&GIyOKD5r}BL@9G5+f_m2{@*))8b1LcwWl>buL6AzBJAMlwv+{ z;lo2GABdD2CXRD1UfnH%EQl^1?)}OoEKZJrvbujS=Zcq2C(4k+Jk)4r0BKF!Gy?1v z3K~+n++C+i90PW+p+os*3#+D&dY;o83!&3a(|ibU$3j2TpLS1$DA#*5&Jar#`E~9b z-A=I4aWYmBD!FcK>%IxcYL5ph9&dL5xuvv!p&i`XEP$OMl|!?##P*h`c~pV9BUTko z5s@(KmF3n5=yC4#&X&bg*qsG2&e(hp7ViPyZ;TBujI#s{Xx6U2YL-2Ah2Vf~(WPW|zn!vA~yNJ+N{C0E%d?QXMNalkPUn80&8PW z<-N@r+2~XKb`ohPe$d{QbnjF8eI;_nNv`d|GVi-@wZt2rObN|w{>KG4Wq$b?kng8J zzWhwMR^Xd+>zz8O7MFc*1<_nk(qgofvUfW5FQzX&?^%P>qp8G z7FT~*uap01pHk$HrbX(;4GPQ1$O!+GOUZ~IH}i1R?tJD#D(@`AHcQ>-J~0H7fK%r0 zw{wE^Y|m%s?R{${oGGw-SdNO(nS@MB+cq5d!f$&w)bsi3S&{^9;qdK~rnwtUHuMsg z7C81{-?6qzbNi>NCU9Epz-w0?$e-vbOGJ zIOfnOE(q?A#}!v^9$lLH-A}9ClKJCzGHES>BlBsjC6ygWRu=JVbVTa(eoK2eqcV|| zdiJm&6VS*y#f}8bGjdg`Cht){tKXo^!jV_N0PV^gcC)+AXuc1|2}Z=_X%yBLo&vRb z0YaQBkstKWmIFC9#Ge0PN1bv?L_9`I zXeXkZa<*wMrx|V4@s1iW=Tal^Ou8I3t~m$Nz?J1F3)-1F;#!|HdpOM2W@aRJQ`SBMrTJ~nAEzKf<4q_aSJ!`htScIbakk%B(%V(=&3{Nri zkB{{dpB`;pJ)GXmC$jY>Jn8mibDduxIlo!$B6kh#b}sf_uF2+)>-}&w_Gujp>kI1_ z9u1YZMrB|4nzn+HevLd7+>fx3kRiR&Jr%uBU_MSF*-vB!f46djp4eR8nr*zIi?{EA z11F&*F?Wh$E<=O*Ct~Nv^*Y&+x8gMA43Q%fARbs~(vgYR2NfUjs zH^=xXjnSUUlzHuAY00AlP=gx<{*-`9>>Jw<*Gz<^n_+^{cv4bpIdKJH>y(ii`i1W~ z`E%@+d)*={-wqYSKkx2y1ktf9)f`>~1gXjEQ829)8nbH-ACA~?`{oxsuc{mAmVn|a zR&R&r*h=cfn68Xpr|oe>Q!KuGjSTa<-{<^zuhXOJUb#dmhMu6KU0yBUx!$#ir)a%3A5u5o#@hGGpv#0d@0Yr0ndax|8?QM?T!n&*+TP%nz0K zOUSXAk7mU$?-3YOmwhc=anY3xKyaDcB;lWBEBJMGuOwhABYq*(4;0SuZ&kZheM)+q z*%k>~YFPfo-L3fYtm?QV)?1DJgxKOxa9-1pPqAbWgt`^~{N1wq)So)dDCpZN+ik=CUXaDXY#3uFumMwk?b-m z85&}-nX4q)N%Ok?`9Iuf!z}yHM1G?H#IkZ$E$+%PQqS+$E2zX+Qt9 zsjU1Qi-D@S@=+4Plizb6fuKf_FgHIiJZi*oqBfgaLT)^N=S zQ6*{C{&HZ_IQGm*OvD7#6nthY<@eqF9?s&iu#bVEHejCQ)bA)Kk|5Cn!L$E6NGLI^ z&hBeIK}4psl*N%SLJ1R}YMk7=$SPg^Zu;>Z@_eJn3*=I=AxQ+pVvPRi)o+r5oQ@Q& zsaiXNFVZnMP z#SQd=*jJse9wHrwB=|mJH-(~KPt9AMKcy}5C-7Bw934AR<*8Bq0d=e-N(^~?NKwOF zH+Y?7moj+IOn+(E|M#;-lEnuO&QND^$yoRIyNWSIBq(FW#>^UwV8XkPH!GYVl`)I^ zOH131!8rcz=!|&~Pl>v_?=Q?qpH~#ssV@P4+k`xNdO2%sdwxp!mDryb>`JX%eg6%1 zr==4#CEHn+Sa#$p`ij+2H@CPbr>;J=0kHY%#fV;$>rUCfePR~DY-%?i_(hSg8)+_s zP^`l&d(F3v9L1;FKkXH-f;4r_m^rpw3C00LN(B?c4*dYbPvN}5Yti2kQl(~Pb9TH` zz#FiO8nu}KbfjQPzI2{fb@mugEs;u|+6VpxO#k4|W2WWH7imPJm;elAskyA|>?_m) zXwm5leEe?5Uiv3z2M43v%BLScVuL9oMo>RoN;|Hl8aaiA(~=>ot=&7!EbI;s2e$ue zkMjGYw}!PzX|ln8?nJy&6XW!ObKHAeC#aBfKiQeQG%StXmmEicT2=miR_lyLe!Omy z4jRA=#)asN4xXII^~1yNYoQLg0e4T@80LU`wguPJk3H}PnceERx1rHT+?Gg*C7H@nT?yAB96@tLBYcIjCog?1)>f zA3pizZ`EHg;NVIDmTDHm0qynwn?`w*|^5G{Dn`Z1uT1R3h$H(UFpg4XI z4VB_(rLDm`@MNr%ITaBaw4d{Ho!tv}4*v5JuwLQJpbvHu^m&5~tl~BJ;nSdg30q=A z*ZgDH71TLZ60;|4 znXX?D?(N+jRFjiScVTMD#O`#qx`ra}M#BQgEl*2cRRe91;o;9gpyt_`6iI>EBX0`1 zM{qLT`mCU{AL%yUlT=q<9?Jj;POPxk0j(E`R{9$yT)45>qiMEABKV;z#&l8cW6=zx zGK&~~P{dpD}ice&Gs!?+PAhpw> zlT6F>Op-+43n|V|+SxHZ9~!pe_Vn@b0Ax;B*W^zDO)pF`d*^14ro9tE8>KX5y5A2^ z##yRV>XCK3NQ38`^A!L$2;1_wNAUcsb14!YdPIA2qg}BP{_Hh1{8t#>H93@!UBMR= z-YIGIa!Hif0qVrv`vXVS{?_whmc#bC_gZwzo1V-Or+$0x?Toz3tFxhLkd>8{XvS=) zB7G_#6_JcSFXQn*Fa1pNWXwnP7WFA;#EDN*3X*8)skd*pdNVHV{yHhY{PG8LwGsD^ zCyA92t({0h`bm%NN^s;4mZ_iZ+P==1os9)QXFU!;B6x`Y1Ss_joUsl$F2Reu7;Ezl zDw)QT<#eAd_AbqH-9qZ@mRr@2YSuwfNMoh5M2-%8a`s}?>RqwVMlKQr?2$zS=%OvO zbea#n_;aN#Or%SNKUcvfVvoXFkiO&%O^uw(m~H^H-`w1^E+A|KSkT9KSixGJj@*Q-d_0gWOzNWCp2hRk~y6-kt`SuU?3E@VqqqGc4bllx? z;SzVbgiLz85L>M^y+ud!3(v-P{MhsLzb4{!iOhdt?+EDlCqgkD%7Cb4@9RunFB);Ew$qLn@p+yYnP1?%gS_1swdCfb z@Vm||t-$b+2KeZa4ciN>b0;TH8};3op$kik9Te}5h!DOHKhwx^8>K7ti*?v}M0f@q zyH!>iKBmhfSXEGWxjegmHmoQeA}^hK+k=>Qx1$h7Gw_yv^izB~GUK;*t_6moZ7Et7 zIrIQSks-prSw~5$$-HAXgJC;Kt%=pabh*_~cUB4xT+&&wS^6ct=rlbXB+gYv-b9uo zl_PZ&8Dgs0`k{`4hj0a&7>9Qs=>+#n^Xo_3^>;i84!|Tf=+)(d!gGD+wP^O|)2E+4 zc8z`h|A(H)?_U=#**N=aH4ZN%YSpzqhEH_}$ix zdgpCpwczW1y^Z_em1_MycbQhq$L?S|qcoGCuWt8$42d?YwNk5epeK#&zW z*RH(tR4t5vpFByYql(U&&`z#MR}&@VDW_?`k}5w_2zM^AOUPp`x~nKJkNT&}pC)Uk zS8KT7X|&`8C>*x^(;Cg3Xz(Cq0Qkx*3V#8}MpyBEe=QiTn%9rZ4f1mO%=(MJe@j>?Vl@;o*HNL=9RK{&d>r z!2l{_r^4+k23sYxbWCf5txiDL2Z;3>oUWtRKzBvmlkXVqX8{$?2|4U0!k*$k$|M8F z;`bxnnHDF2YVmtI|NYf)YEqbpoca>&c`Z8|5gE%yOy0$S^Gfik%5Hi&u-$ys$V=nW zngr{t+%00C{=D@h14$2YssKar3bhu`aZ59+vm-yZ6~}c*aHW&3hM(Wl&8;f?{6g~A z%aeSsPQb)+X*y41QD%)dHNZ#0O08jNV4O?f?oR$?u{RpN%6F3#hU*5}`& ze^yAJ!AG?8A&~QBdicdj$ud`5(d)d?lGyAFmO-U*?GJ?C0*?;Qeibzbs74^bQhW=R z<^GBH66s0l{2w`tDJ1D7yF(mAP9o`g?{x3;@&QO#glmAd_FSyF)eGWeM6iy4 z2JS=+cY-*bmUKDcsr_GTZ{bx{_r-ft(h349NH-`VDGgH69nuJhfHczGh;&N`(k0!E zbf|!U2uKM?clVv=`Q1Cl`v<%Xk3&4L_dYA;nsctXz8^lN%xI{Q%ge*Y{Qex1n1lkA zx?DcBHJ$63b&get!`VxsklMXCp9`gDEjg6a^0yXRc>U?JWVD4|Vh^K~8!^648oqHq62Ungm^;p-G zTT82FaZoiH#B4BO_p`rDl)zjlRzxPLd}7I(`+S$fANh_^y#h{$o@O-m^GEbjEQ239 ztaUWBi4;c5sYkp*)!mNW5=t*-rU1DG|2~24^ePX2g(OX85f%Vkm)Gg)? zOrA<(bvtH{;|E`uc zgZD;Yrsc8dIYmTg=SB4DYh90zybd3YGo{U%6kaxd`r#}<+|5v-@agy&&U7TR9D%`K z)T)}U+rq&H^tUsy0@?6Wg5GGEH->JU@t|~aA&E9vDfexJM|AYvm1%q@)EHBwTp(sE z&FfH-pKtaK0&Ssw?FEyg^>Kx`9H1|;5_)sx<4Q^t_Bc?hAZNLo+YbhINnrZDFATe% zC{j3H+Ne|${K|=$Jkvs@$anXGsj2p7MqWH+=)X1of1x5-Tk`XYRAi-mWcWF#{UU;} zdJ(W-f0{Yih>W>-d_0N=g4Hg>lDp&Y*rg%fN5||2^v&yq@3%qeq1L)6MJEVmLPt%fR#aoMf+X; z#~b5;9Cbz7pit@oX)aj~h0NES&kQ;?b)+nZv=1<2jfhA3Y9xeGqCoZS6KF)zsl=iL z%8(Z0P!{L(IL&+qo?PF z5}%+qB?cb(d|!;p@wONG;F$Kv%s_HJUUcn={p;?GrDU!h%|3R?LQ*Iz9&O7j<2be*kzw)E}hzxRQz=^-1utD^gTyOej2OM+L#XkCdJ)VvgoQp=-&fw$2J zM=oJw<3LbVG(AsDc&BW_sX~oeRg||>>!Tc@@#`u^Rqa@tOaB^6|Dme(GSn!>n@zMq z{FZjiUA$8#48qt$F{93&gRZ`Xk&KqZdoOJ%NT!BL%_vOg((_3u{>ZhaEv8G|`I&gP z`VC%8t&qD%qs0yd)eI|p4T+!`!CK<;`!P0?lzfb+J|P4)geeh>t@T1s__4Aci+Fh zJ@N(H>5@H{FGES4Qi0tOM^aQ(sx3(Ta9lXc3^`XRleqm>Lbn|)N8WKZaa}Hlp?ZwK z6DB*-TolZSvTG5PA@xJNQ74vBH0n#5>7oZ~UDVPg$fE8lyA8N{sF#9gFB!G+n9z5E zBxLyS3{OlS==jFO#AK9cd#wMfT#QZqWVAGFbBFv-ci*tL181k;L6;Bb%;UQoQ{@w1 zva0Gb)SZ#m674<-rt6CFS4q2|4<$>w=bDo@&V)I>iyvfd%%rX+O0mgWV-QoKJ>m+H z>LJ#;Fg7?!8SL8KEoULZUUJ~uKNIXWW^e2ZdGp(mUjcGkgBIrJ>>77H+_xL9&o@A| zL`iLn((7+raxcMlgyW#`6k1EHo^^pF2iq3fu%W`zoQtLiDKU~g|ASg1p291qPtE=WxI?RISF{&77=>lQHR)Oj0E`f!GG*6y9OGy^xln=DYA|cTm z%2AI!JpO|Axvh;uWUQ~RZ-}gSl1z9ZHP6lE&oM(m6aYz@EMgIdj9KySG1HMD%Zxl~ zEv38TPR^mC;4WsYVVUekC%?y_k=n2_9FTFT$Yi|9c^_PaI#13thdnCN|Y?rLYxyy&dXr` zQ>hr3vHoHD@BMJ#Pd zy3uJLzD+|{(Wt?|)a|nN>d~B^lTW%YmlF3nJ%}kMzZb3;bmdK4mjzM@;r*_*AOs~w zTQzH#LMk8$0;(SESXa8;x01)?|HL$Q=_!sz^J38_o8+L3@11)c$(3lF3`Rj(%np!Osyvu0=BJ zIidZ|KxdJ>Fvj^*XZ4Y%r9*tWj~?*P6yoS6oKkH(nx7Yt)S-?SB$`R%h5s18rqJ+H z6@OW@pB=odUggMZc!a4jgjXk{A7w;*(EOrtdqP*e$~;87om)TZn2`GN+|Ca>;WOXm zZiq-cf~Jl1K+g`@)^l-Rxix9S`;)IL>aO?YNLof`nfR&8SI2nl=W>y1{%E~-lJp!x zAt|F!XZvf^QILRMVU%WHlj_ zb*F+W^&LyG#D9w*Ngci)e)*O1@~hz2JrHGO2L)stcnwybQnIXEHJ<(r%bwde1Vzzh ztjftDqnEY|uK*>lz1EW!C1KEuRjBa+GHe43%H?8E8YXO#mx&O3k!i1c#R_p$(2uvb z_sXPxu3HZocf49}99Kts)URgwJt#*(fg9@en-4zigx~oMh_0DMZw~}WxeBDKkXnyQ zZ2RlrZ)3jAiT+5$ZFdFd5#k-%v1q#h=ei1{f04PbHdJ2{dpp5by{(s|uDHs)ID4cG ziRJ76s|DzA0l>o%DM^_nUBH(gwnTKA5%F2Wl)1gHfBoWmqMGqvQD|sbR~*!}9rz*u z=Ejf~Rj8W-02xB#UQyaaj<(0z$x*kUqhPL^q)g+TlO%(U>Dk-S+H-wwH~KfURgpP$E)iWiHD+Grf{FgL&BC0*2@ z%Pki2!eZR2e7QN{O-Zw+~bo z1m?~nJqLAXX+V(SvGsIfp}K6k!vOO>2q?#hfTHR;M}^(TW764ObgW&FQtWeSJ@~X= zJ*pVD=BV?gO8X2{l(l9-`_#eQT$*CT`+kQQGm1Kb47jhqXm8XQ6_cy~lu%iKt~iq(yFqQ9z$|QxWAQ|E$RnPv|hZ8N~dTK-~uMtMd7y z`(tWK32g@gZL~X$8>PYCw(*{&01IJU0^$>sfPg@*MT!F`I`7x{T+}U#;|WzSv0UrU zohN#BlFm|HoS)~w`ydVe-)CwAqSHg23DAb720)N|15i~grfaRNtrJLjY~q0%^~n>& z(v2?=)VnONJ3~XfdaxLdH~=ffW*;q%>g%3A0%WoO;c+Q%(FE{0-Sln>>ul#(5wEcB ztZyp^4({$cz(EJ3e}boVFT$?L*f7|r4*~CTnm9Te@-djI)vv-+7+uN-$!9mOOTend zLtb=n1_YNmBAbKeHX2zeUFZE8r(=oh2f7h0luY1qM7zs%!kGEY>SClLc%%G6Cv?x-K-pRMO?jf13?`J zrVTl7WG8g$kA#;7%s$}ojkVlw1_ILx2;Y4%Xh|7lZ3+i@O9UbT=3l8aH-f3*x?Y&S z1l<_5-UxE%bqF4aW{FNWg4_kl%TQ>yRNA!tVQdnZeF$*oAf$=RW*db;xqDFLJX_?0 zaNC-K?O%|kXUG@=t8iim1hfUW|CHd*R(!6_&Y%%!*B1enK?ENs?(#t7<{gsoeoRzX z%#itcFsaS_y)suxV`BN}dDjkr1keJ=nTH}Fr_UuszXIkr_N~)AU3K4yFxuhTzSq z?pVxmDV#;!oJJj-gK|Kdb|&tvqF(m374w7~uqi%atwV3B_7Q`#*%Zb(Wj~yPe)5vs zUAM>tr`%#R&%EKYVT#J|^+wxuv6vI$3VgQ7fLeN4TT>%Bcm-PJPBU}NiX$klU|W`e z`?zdVcl3EF-v#LJB1q^Wv?&7IeyQ7oEejzG@E5_ z|J=C}Uzg*SEcVWLYvT0#rBja;ceaz$vmLvJdbe@ynqr)VqFDwfQVmY$8he3F#zXa> zgQyn@4=E|lKtbASsyY-wHbKxm4$?gVmhi7{CvoUMcOlW;3t{;$XLqg)(u!!V0k_L_ zewk3_8MKm}@n$#4=PYUD5O}GsGaCjxwgL!boBc+~JJZJqFrSIH6>QY{Lo0Z_uY+lJ zP`fr>=3R68%}kik>v}9-Sfg&}qF&?3SOp7<4aoL}NpJcfX@V3wOfYC)Frl9x-9G;Amm2+6Ch?#p3+U+1* z`i(kY+eg(?_Nzdo%NGgE-f`Tv&Ed&OFn_GL`pK zBSwn~oAg-KFsA~0R-&H=VJgf%;#0Xj87cO(*|NZw5TriB@6t;3JYP~97M&CXRr<Bx8hcg ze&y~MhgpvCodpLbgDhny6q>&@fx97xfrA*MZ!dO3IrblPe;GUw%vsTh7DgsEx@IVf z)kB}#NC~zemNuP^?OJcNi^*!8$Q}UE9Ma~V*&=dc!Es2 zxOh=b8zJ+TpVHU^QzUh92xZp!^HPS{sh-xFN(M@ga^a&4a_tpW+JywNa@h<<*IPVn zjL+dDaa0h0Wc{-+UTHW-;wA1`m-c57ww?hXS04o;Tx@)N+V-8- zdr>~kuiIvTRzp5cDrh&~jx(#o4F}Y-*IacGnn|4>WJ$D8JT zzBY0=KmST?Lmfk$%u=kZwD|nyv(3%#-^7^&)%b7B%q+c65MGy__u2A23*Oe@ zMgQ|+=Wlhvn)$@0Y3teKrrPIoqGK2~FTDZcXt29>aF!JB|C3JaTU@E+u9t*kpzrg{ z74! zIT6PkFvh$Q`pqJ?Me#+f>LPn>_*Y|1?0KVWgzLY5b$gA*Y#IUS!|3CILdU56`c5_>yu#LMAlq%!I~snpJ^}fXXTa0>jZXB_R~d?Ym>9d zA@65J9w)VREfEOvs2~6fHfNf6+*|%}muXjKDA41McO3++=2P$iiAv$*Knyy~9FUxO zP!E&Umo;@P5!iSoO02@Q-w1$e%h+^;qjFai#TA%qryUPk%L_z|vk(Cu**q=amS=!I zaPTjiTM@qY&3ul{E`cc6;yDD?$^Wq|fgM?h*emn<8XqD`H&*>(bhu4%6gr(|3N%wn zH-uvxX8L-UALU9NQZ?T@hGH}Ghl!P64h037E4Xj#k7kXSrIgZg-yW@`3F^baozFZ0 z6(+qQ+N2?=>8#vQOzM9xHr*)lER3h0bsNVRSG>>M`0pgYuG-Nwh!@k2M-PBcL|@u+ zth4vs*8AOTC7>MEVq+FFGc&cgSA*8`AHsYeIlR8QNL^nH;;%hoV`Ed^8?M4vw~a)! z!+Gj9s6aAPJT(1S{3xy;Z06$Pq9-ii`s(Qo5)f+5^FB8;oU=rb;^SLeTj}v_>E?j9 zcLq)1)!=n;yf`~XUi9>lo0v*?(aDobVJ?m0K}-elQ74IcYwU^Kkb?l>*ELfbCBF4G zxkkF$X7$Z0ZqiN{30n;6HAMGVY<%t`P|#!%JpvnFs_KEy06W%6m6!cPKGz54=jQ4^ za^W*9;M?uqUZ`<+fQ;}CHvwT0`%>RYL$dVq0~o;p{69BGq$ro?zXmudeayJBV%3-e zWRj<=fwmY*ee}Hs_@DXBg^sS;uNC;q8Mf1<*Q z6zZ#vEE^S{{mzTGmP)7ZWV>{`@$oRDpp$Z1IaB&I6QZMP*w~n=DYu%gkJCx@E}*7L zI_#SF(5EMF(nz z>6}c?tT+Am_D5vDW%G;Xk;;q)zfZ>d4_^kpop2bwG5yEmvvcCzCf!x@CP<9>Ls%Fy z>!YmC<-4ybi;=S^|2F=Z)vB}k^hqH+b@l}s^5p`aPk9UN$ocx)0S%!^4(ll{6A&Qx zC2TT>Ev-R!U6-r#qX|$4MI0l{(Kc}KvT6~w3Lr=3vHvTv2yii%4IqQKm2I>_YN@ui zKAb`*1@cFq#$&R3qbP0lAkx~lwIwUCa@*^Eum#sZ0-eplO4Gsc?yv`>Af<`%YHO+n z0i!06SH=JF3?$DY!1}m~m(Dg?Pyf3*nKJ{}3IYJ@Y+|xFo?k6m)FkVYdJ6^lvgqR5 z&dK7a+A>o~(=XhNDBrmqE#GGGtpp%0`%uj%^2CqU`7I)C>%@dHja?luhRS)ss%#H{wJd^eIi!v8`;6C@aLjGuG z4%mH`vyp%iwn({USMG|+ide)E$aO|*&%wkewJwKAJXEhsC+lo8TEI8*04g5Hmp2`p zS1v1%R6<(B;3~v<`;Wy?D$kHNWTljJT77Q3r#%EIAX$2H0s5ia-fcUr7v|>Xt93$u z~ zM1E1bPYLhrA~jNMTqRw4_wvnfeX_&gwNF5g>)qNgylG?yP0*&+R*LD` z2Jn>XA=KIgeReqB)&o?cuD{)3Sm*)aq&$%U7h&>wxS4hG)rlXl4Xp+*Sli zCqM+%rg4qozvpnCG(|%}hU_3`Y4g5p9gZCsZ3K$Rttl}Bf=CMYRq7Mse{l`(Rvv=q zo>9{MElV=29>P+%u9=#us+gJ@VTR{E77TgD1lbCUQHDF@{I9`j`DK0cGgofLHv_y^ zAi59nG#??*e8(x=mLRnHX76wJ)pp!_I#FJ6?OLnk3ZQ>oAmVeGSarKbhFQ-81#d8h z2L~B9e_Ekk>z88X_MHQ2tW#eVkZH>H z#15*52!9tl2YTc?1c9kD4^jcw7cR%zKv(hyW+5k>O^_0#Sb@0A7@qGK2B5o8@Xr|r zOfxo!YEQy3C_UMps9LkKDa1kGx(i7AT}UBKc!TR!rP!Pjr0Vd5%Q38I`a52|zg%lFtuP0UH3GA|9f_J_@+lwBgO^jdKz~ z7R1zZ_bUeIEJtGzGAOI7OYoBAm6k>#rWH&XGa~5*;aUKM^qe1UG%P3TGCg@hPj-jX zbb!clY{kkA;PK(`WA6ae%VzPD?!$)Uuuu03bHPG;UIQdxKdOYb2-s}T8A z`3&Jtv9Sawen<%k37`ubhDZWgP1hophTqmVnc^W z=fnlC_oco=<0f>fg_YIPx@9RfL1_JtX?I7^OaCEwuVs4wJ@ZV0*?Q^InkV`OE>LNF zt<&We_it+cLq`1DkU|#ac}K#xVVYF8#fRi9j zjXn<$FWHS?Uh*T%j!9p~I;;+D-yaG!HZ%MeklqE|VEX){2M->sNKl!&<8b1libvsR z3t-psyybr0PH0aW#Wr$h*g~aTD5i8T7;jjmW!#*b=Wm*nL__M)+3p6JEyZAB$>w@;db5SF-6232UKylxh9^0RaMg~7n&<^+qq@e!(@&+1j zwXytvogYb3;ZqUgHb#z3VGLYME7%l^qcA;I0|EMz!KOKPuZd(`NmQ3l%`L*J!Ndlj zV;)O|>6Y2td@0`kyTT--1&o?7xB%1>ZsQs+m#ux&3W-L{$F8hh30mL!Wd}v2@7yEk zfBo|agCAmhsVIuvhKA&V58nDZIC*!u1RlX1rTX~z5v!pg68gv zaN@7;@6r+rxpRGe75$v*IZezf$o>a*8iA)VDxnG{D_OqM>%tkz)e4J>j5mK*5C@A& zNrgQA;;6?s$`$$|+a?v9sL`fbG*SE*I!v+aVS6*->r;~pNDID!pgW)_zp^qm>}e}+ z8SS6uzXgm_fB)W7G033%4s<bTQm0S7l+fj`lYwXt+!}Xuuzt>0MTTfOB7Nc?BEW_NJ<8A z`YUYJO`PX=B~%ZO-e>rf*R^uc@Rj-2{~j}agqBzMY>Ds!5}^H4m5$J`dysfsvc7Sg z&>Y%+R%Z9pkD+D8`g8CmMgRbOD!{8?kpI{oh((Br9$>>W0UYZyfDrz!GQ%@9H7%9q zcitp6^gg4Pl#~Q^PXb_-T`oq<;~@_^Iy~Io7ekxOW`yG1mH~7c#Wdcp@axP^p5#_n z6X^z#A}(_4)-CxIu24A0+%#jeVE6l;_*9{8ov3a>)(DfnKB7k^+T`)k?#lR6rO{M# zuR@EK&w1^u%%G{*m%wp)~SK)tAJ|hpB)xs1u6)8Rw6#3t*`$b z?h2k3bSW6V3Klr45g9GdkPJo*>`3X(U0&@aMx}IlIXRq=JLFk;dFXJE$=;qFfIOgM zU1d8b48z55XeApMiI6Krn6H6>0mXgopIgHy{agq-S}1_6X#8@;(3kk*1E?@*V$nqV z{K1YABZ0en(+~EkezWCdb`jFdBom)_?P*ZI2McLgEi>lO8hg+xSt5_WbM;c{-Fn5g z`i5`9w1#TrR~jsyaOGel{IG6bhb2sCZJ=1>oe8XM1$7#|h$NE{7e@tF5V+P4r)%L} zxG-~J10b%WLkvbHf@BSY34Vnjs#kvxJ(9C7GGob!XksI?12vrG$+ABVE3dU?4%pwO z>2wjFYl|`|dw?dp2%L|z4U>+kUYOP1d#}hztk9L3{6bD3`va z)}sUm&orylj}{!pU6}BTWidXvRPUt;&=znJ!AYrDVOd!nkL~O?@E!wy@TMy@D}GB3 zjIAZJIK*TVIyAibb!Wuju{y(C9yGI#1C($|gZBl4%;wCFcT?vJlsqKLivA@(-#NK} zNe{q~ba+CbXUM9k+=cxn*hpl#|4wQK&F{_@oR&9{#+zwSTX zW~x>HkV7C11BmMxw~C+@G0>f0sOtn@;P`{viy!!H^>dU^x607_X@1q(2_ETQlAnGL zYZjl)->I5tl| zutGN*A!WY8&o1awc%J$%tt2pdZiTj>X;NKmhP5se2?=jdMnY6wORoC?>;@70E*#ud z*MEnOzk9LwfCwL4J|?EmRoMY{OcHbiU)gYB;gF_}Ls$$gu7Th7i%cv`pW$%{85Qd* zq8Pv)iqeU}(SM^88yKq}1>HDkgYkwwI(NzErlB>3qQQjFV(A%K?8RdCgHcD1BJ;m7 zhx?qV&^HGCF!_(h#Y4CJL~Dq0{7q9%aRYpdq;ObJ*)F|hWo5~H7~3*@Be!qMC-W!R z(RD!n>dc|{PIU`25w?new;(u;L=Wy6Y2Sass0i$-AE#3~JrU&7)?Nct(O^pK>7yel zMHJbHiTvm=S60K)p+9~=s^O`XFAk$(mwKT^zBW}i})3Z%iE+mcCw zO-59h#j2f{DWdZEY&KXMUa?8bh$81iXYMa-r1>PFa84x zvK1MQriwi~Gkq+Iw+Kv-E@O82K^lTPgrO3c%nM`VP%v5}2KqKlBFwb?f(TsyO#JR(zo8u$ z&oIHr>ENM|lBNmu*W2>?4$pPEtWlX*AJODWL=UZWB6Td3PkWh7KND`7tI)cIa;h}a`ZlZ=L&6(L}Fym?MS@1rt~=_<^OGESpcsMj(_xnW@K8YR<4Ypr28tQ zb{%NZ6qFFi5UL8x+ISt>6&+u&k2%cjEdMI0HF+GS!9?V6ELvjeQENhsF5GEHV_*Eb zrq-GeE5zch%|8h_9B_3WAZ8b}u?ztkby=XlsAqRKBYpjNR*!;eabP%%FvGj`k+V92 zkWj3fwd0kiBinW!euhgE^Zv1?V~cMp?T_8FKk5wN{lS|Y2{Z)(lI~yr*EzMSI+{Je zHY0S8;-!5f7SceRNsc~48vzwdK{=eOvuP1u+@c1U@bv;S!;v2cC0v>W&nOouVxvC%Y zWMKLvnyRR1_XEet6fO1t-Z^1g=7euus^--Rbv*iG-*a6hjEw59H7weRl5&=h^DGd? zpFtso?*Ey&rASz{BZ)bM{2pS^l?T(r#;^XIYZrYS22XL=#K;6=ka4{{#= z*OjD%pRC9pG5?jAqvnmOu2R>zP@Y447ebH!zy1WiqiiEX5yf|qKvup*gtFzp``?v$ z=RRUKBa@*w`eKY>o8@Zh^WWG6HY=-EXTj?lD`DpsN zJ>|yLCmk~r%H@j-&)b-&hDCG57r$qB#`q13IPz0xr$|s+RaeYppPCAcHFCNa?)(xZ zJDDZEb9uKOi_JI6uf>4`yiitxXmPt)!k44Jwad$Fk>5}b>Z*HeqX;Yiowt}*68hDI z{jscdZkZ3=%vc}<)#uiqQnc@a^H(ez7q@or3?~M7HQgdcwL)b@&H`bvzm5&s4%%3J zZ*m*&Y-9WyT);=8&Irf2rj4X$&tk!9QyvUPo>;QeZK$TZXI3$zQS>c8M_2x;-Egc@ z!)%yut@so5ympP%)5u%Baz2T4q^BB*y)8WHtNE?+(k`iN>d%UAhQGVbMndRrA?-4K z)-U#oygbUrCpNU*)|Yl5_O9UIbUl$d8Y_y;!M8TT)05|MIxALG$Smlp&u_DF5AOH+ z-k@JLk!{LJC@dmAJ{9H)jhZhdvaVg@i|QI`>@T-kqeiV`aTJs2vF4kK{-~tc6syul zTFrZC|8k;NOw!;~oLIW^ibOfyXy&G{c2B9j&Yp#dU87eya@45i@-M}~MHSsu%gXPE zsDj%cGM7|b?X+UF3?3z`CE!O0)j(!S1Tiuuwgy*^*jy|DzdTWyI0e{&>!{*z^M ztz+ed9e?OBI#)EXc2!2wVc+=tiDegwaYlsnqn^D7mG)oW2i%pZ^7YAVD&99ax>yk> zlHpm<8nAjr7nq6>ED_+Yd&fy zb{PnlldDacCy)|}actUggpVPXp^&GWPSHh!>|>hb8P@v0_;>tF1pJwD(4Sk&$^JzzGT}~G18D)i-*)IKEOQTA2w)YL~8P=`n@A%i5fk+?6 zdxbA^dKGv{uO$;a=sPRJ#wr6FD^8l%e4;xSUl+aWqLPd~SvdbjLKB<5tM$OWQtydJ z7w3*oftw}Amm50833&yP3&Wt%Po8qYro|ju6z2j-leN@@lZfY9JHj^B&%gE>JmY6;fl^=0yJ=G|e z2RzP%@k=#ElQ>zXu`dlxOB!i96;4*EqDD8?74+25FS&OukDNUP0!ZK7Fh%jyqUeaFw=}T&IZj(Tz8g+@N<&C-k_S>aZJ`{*YrND=+y=q13yvVELsi zeN_5nquCH%VZQP8hk62m9Qljx{l{c_rFmthT88NV`6BImob|sBrhDvlHg^`9uzOe9 z2X9pFhhCpv;M&$3g!KS+xs~Ty2kWci5xNKC52n`ds4OCi3LYL+R?0kAMP%uzK_pXr z_j_Yps3SARZ821A<%JyQzG5-6ZE)RS^t!Vs7;Y@rC%OOg$_wneQC&H-Ia0oOj+Nnp zUO%HUkm4BN-k0>LS;mh%I2)B^xps~|Y7QGMc}Hs(LJ*aoI(VV= zi?eM>$J8x3mj&|%D}UqE-N|5Q(@b|X{x^$SVnn`uS}t;H=e$Xv>1eP#ZjHWpu7(1w zILkZ!vLwHBRC1#>RMAz8&^Wr+gH^mEMJ*ZmL&IMB{)B`UJdtx9RlGByKHcpmbmXd| zIPTg8#d#P?q8`^7DZg+*ifEpEVjhrU zAkcnfs6um+D)g_cmxKdP@ui~S>T+BEtMU3>qH5{~Tl~Wz_-kbwY@Zc&2JbwE5#a|uxVlg5B$vRP*7Wy&I>Y+cu6*Kq$!ujNtG5)IcCD+1|-!2 zIb$)>O3N1AY=$J#kUk@|4T*Xw&GU|EruDo#q>IcL%cAJj#7WGT-eIulA{n?+5~6rR zxEjgO+LB?ttui{9?ndi-bNidnE$cps+O6|!0@tLI@@Vzj^CDiTYkR{TysgT}WK!2# z;w)egmdz=}o=v~oCFQ=tAj^>auH~eU2{|+l2NK3L?)h9{;d8$V-|ftvWL4MjUJ}07 z#ZDKCca9h=3$HwI7`?(uW@SBk{*8#I7(QIG->*F|tx+H+-ib=v2JEU;W zrsNbyFo%aGR>yr2Rge;+ga8734!j)EuwE2*HxqL zv6Qk9kS6>lFEG7iIC^EJwsN3AHdiAcB*rz3ST_1Zn^1V&9-I_xkt_cyUnugFg47;q zLixIC@4P-o2s5#KvZwx2(X^ZaDSOWk_iyi!1^QAho}SUGC%+uWQLjR*A!wq!KE&a6 xl=38^y!rSU|NYPZvYXI3|BHXX@k0N8gTd)#vZ#o(!GQ!{GLniCrDCuA{||7QOmP4J diff --git a/app/src/main/res/drawable-xxhdpi/action_bar_shadow.9.png b/app/src/main/res/drawable-xxhdpi/action_bar_shadow.9.png deleted file mode 100644 index c4e808346805044b8b388669f98b306f7f50e452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^EI=&8!3HF2>gs<0sUS}m$B>F!r584GH5l+PTohTT z(DdNZlLuE0ItsA-|9|?`g~tIG4(Rslc)HB?Iw$$J!uF#=qI#n8!U-EDZJ1y%*P>nuhteQu z37%@jlwO(|L#dFMhM}n`HfjECMcy_AA!`yhb(8fH;>^pNw{Lgf6A$~HX6DVl`Q>{v z-+X@pB`HZsf|asDe<&)5Q%5Z|R1qc&q)C(FF-a24%BTMskf@@AHX3=xWm1L(?((zz zGhiUmLKg=q_c(o%DTd_3fPh3R-82>O6=oTfGd=@}J)ESyh`l<_6`Ar4NVIdBaFNGn zjR7fr01_eI=BVSN6w@q_WQjG_feIp2Q%j5kR671*gl}c0C?FB$94$MBR~ctaF1q}# zfg^Mf+4104F3RAZnNh+f;rTB2Y6R)8U`=M4L`p$GJt_L9-sMo%HMfl0oQe-v`OtmWt^ z&r_oe?I&VQ#V`7(CLvh7&r`-IVB4vS!(ry{3I6`yIDrSkW z$rYCkX^ug+@A^5R4)!t_Ji8mreS1Pn>_ON0n?c!nJ%dY9h=DI&C|3kqhQDMy}GrN7(I9iksZ> zR9Xo08gJ|9P^)_9^~yh^)+Uw)rUMHF8EM2H?)4Y~AtLWjCf?=|Zan)Oe zI-eqNGskHz0B43lIqP+DzGd<_T^#ZpfNwCxjh#2Tk3d_-;4p2(Y(b_j8Rn7PaXa9? z0c~OJB2GOu)Y|^lB1w`2bA=bPN>Y-N1S|glAP!NadwwfJ00000NkvXXu0mjf;wNeV diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_add_pressed.png b/app/src/main/res/drawable-xxhdpi/ic_action_add_pressed.png deleted file mode 100644 index e4b6217c453779ba7e5a9249a06f734b2867a2c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1520 zcmV3C*0*(VcNby^`3G)0^KnJh}cqrlCr+{8y4^o^>7eSuC8rTIi7HB5)0XvZ5 z$I>Rq^B)KH0j)(^Qx5~%kz%Oy2=e?k-~donWXE#@cm*lGDh+}>e;)7=@S4M~hJn+- zMc@i>9T)>9bbSS|5Lg1#0u4Yju-M@qpJ;2kQ#^t^zX~`3tjeMFsGb7dNYNkhe$N1# zfsLBLFnkZJM~aEU6Xf~Tz<0n?AuWFe_5cTv;#PvORJ-<&H6g7Bfwf5Sd*KLDmUAYA zoJpV)kckoV;B!;}64(Zmhmh2a6yy0PNUb@&A=BzQ(25k73lxaf0Y`x+EvxmwUJiz{!9MNa+}?mvJw;I8AC90^ie z7L7)aR!DEGR2@ae;FgYPFzBL-|;WEojVftdR(sNHIaC zq`E3@HvhlHoCs`JqTE%#n4vbs7UUe;MIopm#RN4vj@3%gfT6rOmDG5C8Ri4e!6*dvp*H1bW3g6Qqzx;8 zMFHivfphKzDK)7nG2NU(f?hNxuu}XLHBdZmDBq|z)Y3A&Dp5y3K{ysQPy{7-nli)_ z?J2UJ*HI^`voQ%AiB6C@zwR~Ec4&Q0sasDKu-#DWBgIG@3j(DMGi?#1;aqGtU^?1) zKTa+-V?}4RM+?F>EnX^B>#5h%D+p(h;(UC96n0orm7`iHP-^5XKxvtR&h@4(W4aG6 z`k)?^x-ljuIaQ;dx5sq*X=CT^4di$>B`i z-GbE3TYxVuEe-;&7FmM#Sx6we4k=FL+NIl$qLTN2YH6_r__`=-l3H_4hLF?gLXJB@ z3U8ql>{zDTB|QJ>0(+~=mT3h_^!|=(*HMM!m8ernn~R@9y)UsdZwfRO1n$+OG&u~3Ps{fRIY>I3qe+b#rz`DYQmx8KlJq%)q$&%CCFSFo1F>6X>;&nT{843Z z>@KA9FE)xJ+jgA9k!#EnoXV>(O;VKOHfUiBx5Gk}6SS})X)i+JIFTklB)v^e;yKaB zps-(>mNdj+9u!L)ycGwWt8f_5CBD5iw@RGomaQ{#B8OUzLxh)fx8s%lI+%M*$05R% zGt#S)BfXU@x`ByaMh54`PhDlyYi-^8(lgwquMxN-00hfo5>ly0_M~k!Elnb#JXmAq8O^ zw2^vnkVIM;`Td^s;G8!BUUomACeBU|&UvfA<1wEx7U&2VIvH*PyD6o$ibu}oz-C{8 zH)cbulyVwl`~=(sZeOsO_;4E-R`WP}W3Q;APtw{*nmA&Yc`YPu95EmbBnuo-pqfZl zIO0GxkSuYk07)WQ<5UBZKxzS}D$p3DR&Ye8JEYDdz@Jud)PUxmn}!|@j%OZGhPK(y zIZrjdchCarhFe}R+%~$`(2=8tqY89|bCUV6G6NccWP)P?bPXgk9Etg$8=uJ$bjizp zwfUr`fv$*Sjue&v#}p|j|2R&baa}2z$G1=A4OCR!oqQ9>RXNx+q0(u9W3>-NJH7D4+eeZMeBCaX$-fW1K zQhvu6zX54(Uux5&1H)|!9FAQMEU|!U9!Y%;xd%xEN9;1+A${j;LFa+y|5iOncYwD! p=P!7k=Xsvzd7kHap67Y<{RQRo6Ks$?-dF$t002ovPDHLkV1m{!9S;Bi diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_clear_pressed.png b/app/src/main/res/drawable-xxhdpi/ic_action_clear_pressed.png deleted file mode 100644 index ed70b4e6b20693d956e2d2ee7b15358315d7c99b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 528 zcmV+r0`L8aP)Dc`vc*`1Tg zCNsMrgb+dqA%qY@2(eW}&hC_C1K<)Ek;YS#Xp*c8+yS?w@ket6<#OJDJ}?3Hf=kNf zJOMkv3^;NjDRL}lPFP8Q<;-y+De@pEtfb{~oJs1`oZdx&9Do7vOp^6H%IN{G1#$qo zz*(Ik>IxbGWlv`aypv>oPnq-hGe!{;;M95zDob%cuUo(|qqa>r^aF_;r@Y^Z8)R$b zVO{VOfW@oqT#&CojO6$c3d~rJUqN*PY=9j9f;J7bA#%b9sv0nSHAPN1 zLBDm=%Zk!qyilT)U2{Mhe}=Y5>3b)8$tCXnKa_U3))PQO_$P4P&G+ra$NL- zwjRA=TBP-v*N>v*k76fa=B(3TKLqx%+6Yjws&+?<1gT->Sx!SFsc9cnkhXn{18D8W zp|+kIminzZykPl-2{d-yQsZI81joRxmw%Kro&m=T_1iKAO8@fab55JCtcgb+dq@t<$y%2~KF SubcD$0000nI|*PStvGOzK<6SJ z$3X&ENVw}<-bjKummxtVW_~24&zk^}(qmU6U9pqa4=-DL$w?}xA-#9bxeiO3+ex@l zK`BTx6RG`WA0Hw}ZF>i=+I`(jKx#QpzOz_l5*x>t#K>8Z77kL$$nhaz1GkKv-lX`P zUL+olH;Iqa47u$jZE^24bkdylo@7!WP)ZWXNk=kr(vYkiev*^JOY-9Ik-RxPB!V;T zd_Xu!B&Sr``l>P>iRNfKSDmzov!XU|h@C43$;{C~ZmznDj1K4YayPND_wbTC6ghUWOS3e!X$%s#Yt5bvaeD z+kXyAISTU^-)P*7{>hM(y6uo>mv)t0IJC;={XAMv?wHjWN=!L6i!Y~jw3&Mr9{Eg&B62l00006w z^YxL+42&!S4qyUCGcdYr_p$C;Wz~IR-A@_+KqrmjT?3z@Upp0qvYy25f&^tkK_iIcMGEGsxPS=4znBsF=|MKm}yMbu?9Bs68z zSv5E`S=D(@NNVz`i=N=r6jhf!(J`ejnCXjZ5T~Eyg?5iri=__vN>d+AVczSYbv%>t z3(HDYzuh6F``#4`3kpuOe0}lrLvgjtpFdkqbA#RN0s-Z*4DM$ZA3gZUf*BY@44$rj JF6*2UngDToY$pH! diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_download_pressed.png b/app/src/main/res/drawable-xxhdpi/ic_action_download_pressed.png deleted file mode 100644 index 2d91bc3fe550e69ac121d156bffaaa4d621b908f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@Zgyv2U}X1naSW-r_4c;4P?Ld(!^QTz zo>{s(X0#mL!I~8-X>u;(pv9@*ukPG-J~1=IQ$SG(2ySI^$gb60eX{zQ=9}IAYXln@ zm^c)`1dMKASd_kN?#^u`f|*C3Tb#D}x|n}{sPp~x=92o&9XHc9Gl5T2MxFhIL=cys+=X(FOpBck^A(pqdcwTd-Rt;D#x4CJOv~eP zU-sAL0UaK++_oyNK51Il{P-Pv7z7*`7+oOX%O$pnS2AV2?><`s3>gMbS3j3^P6V^941VWH9iU18#0h{n0T3qu;sijP0EiO+aRMMt0K^FZDFHKVu=+Eg6ah1Q0_frd zfRJ#=M+yO;BOK)?5C*X?g$ccU0N8~HgVaf30yf5v2MGO-I#-y06?QpVdy2=U>_Kbx?{f@uvRm%x>OAnY^p zmy9bxsf3O)KbgRrpG4r!j}kgG3ag?%f=0ePK>RP1uoE}R`P_tz6Ir92&v`b;%I70w z=W`Lx%I6_?@;l@1m+`XNo4*=tyCx*~^UY;8P4}EEiOn1vHZZ| z?$X%7$0o${gUjos2`TxJ2OLd7LR$WrkC<*~@*@xVUd{YFqWr*4JeA1z9!95llgRg+ z$jaqAhv%Gp^HQQ@zH_+3M|!^Ty3G{tX8!$nr>|2&?h8+)^LvELymB^f{(*40m;T1j zckY6(U6#qufNJMQ01pa_yxREzfJVL-ppx$a=;WIKrF;YM=$tY@Ex!lQ%Rd1q<{tqx h^Xt^9Q>Tu|`~Zov&YaUBjBWq`002ovPDHLkV1l^i7E%BJ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_action_disconnect.png b/app/src/main/res/drawable-xxxhdpi/ic_action_disconnect.png deleted file mode 100644 index b65084874b1d9dc7706c7a2154b1be0f4ba5de55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 642 zcmV-|0)737P)NklkyXI&d>SPH#tDFN0$`i~7$*Q8!hvl-ZURoQy(SBucmhC0z}o65iOY(9bjkPy6p2m)Z7;H;k+ zBRH#v#t5rRMT`+#fJ&HA5_|wt1ibkGh6vbg5|*B^NoYJ{kGIbkFyya6V9D=DV9M`6V9QS= z3<&en2((iOh42@E9efxjUwn znF(?EVSor8GZ8SrG$z006*$XuXjAhqJ!|#Pz3_&!$% zlfVBkk5ISgM*y7Pu4e~Z_j1QO_M%pG*vqUC>Z~{HciTxW5o#QYa00@2JN}#}ec?!i z6VQIk>={A63W&S!dM|sv0x)%<$#%X6U@PANSj%4l*voGKEaoo(Z03vLc>b`NU#3i% cGXG`10r>eme2{Y?(EtDd07*qoM6N<$g3zuPLjV8( diff --git a/app/src/main/res/drawable-xxxhdpi/ic_battery_alert.png b/app/src/main/res/drawable-xxxhdpi/ic_battery_alert.png deleted file mode 100644 index 6dfdb8f734e3c6daeef6416ac7cbf13b22dd5821..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 249 zcmV9uIomPqf*QoNiqa6XfUvmp#LYs4TiPnUScuMNRpwPfs+g; zePif=8HnuJAhHZ&VBlg1#uj!OWCxiBHsjdHHjaHX8IKw_Y8)=WLRblMMT4s{8jXrk z<3^1eHEuu(C_;tOXjF_=J)==EnrBCi8$rhXB-@mq*o?bQwsF_68Mlt%8(D^ZWmpGe zG9yHvf0ewzAjBX@OvZzQ9}Jfm=A3)QFeCr~!jB*ti{k>A00000NkvXXu0mjfY`I}| diff --git a/app/src/main/res/drawable-xxxhdpi/ic_battery_full.png b/app/src/main/res/drawable-xxxhdpi/ic_battery_full.png deleted file mode 100644 index 3503f6f62c1ac687354ccc1d84df6d5f414cf0be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^>Okzs!2%?Iu$TS>QmZ{(977@wzrD1P_mF`=>qGeo ztg*99rX;ewf8cwAc_X{>2FDo(O&1k-v*s?k5a7DRfWt4dp?qrTwc1_Q(*HEn*6aOM zo8NysUG#F{ZejCFr+-ayX#Kt5pWC9o={(Q9ubtu5-TJ1gz#_*Waf8B>vhbPV+fEgp zJ7Za!T=?inr?9%;919TRcjh`{?btr=G`*)gnAdV8c}k18*%13H_*)78&qol`;+04h^pcK`qY diff --git a/app/src/main/res/drawable-xxxhdpi/ic_bpm_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_bpm_feature.png deleted file mode 100644 index 44bf3a8e17ad806d1e8b7a602cb54e3973a0cbf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8685 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 z 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 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_csc_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_csc_feature.png deleted file mode 100644 index 5132dc84b6429ddbc8244d22a4951c7c2cc4e575..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9654 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 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_dfu_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_dfu_feature.png deleted file mode 100644 index eab5d758cfecceefc7aaa436935de4f04eaa177f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10698 zcmc(l_gfQ9w8v9`fYKFEstA!@6zNqg5Q3mIrK2?IJppM!Q9!AI(5nOx>C!@vAc9m4 z(gOq#G1MTv-@Nz!5qFJ0PNl!lfbF<86?j7e%txhK2}lhnqa}(L@gA}b?B?!B8gWVmpN3$ZCFiy zy;rjeKK3z{MpR!D6@T^WCGdWc*nO-JjV>rMQHwDmQcG7AHnhColNm{4JT&P%HdPj4 zGu&mb?zVZFb2wCP?sg$PJ=Wx!Tr!hlTc~$Qe#RX`$g}7=wGN?dv(ND-5yGOlPxOd7-Fz<*=P?d@pM! zd)liY8ap$q!VbP_Ia#_1jtRaArimJS)s~#%*&k-S_ji&a@W(w!=umLhS z6>mZsW9`)IfeE4V096$ePc{LSvFBEowA0ruyfy&De2&UX7NtVCM&?7CeF{4hcd;T~=yL6~yz{H-<_h1F zimT-+I|o_57(19SVP^p zzul@r$eWVhz5GV5Q{chN!SF?0QZjxM%C~tP39;+GeIR(|77pV=w*?Yg;+6urs((Oh z$3*Rvy`{Z_X2QiMXCE)?9c5r2S|4oyB3pBiX+WKF5bz^4p(-6U?{Rx~@!hh@3j268NpcMZ85Q@^)e1Fi$VL%ibB- zD%{Q>LE)?g)?9GU7$lJ99}O>tJ0fj}PJ$Q_;Wi6`z@m*oKU~RihXsxBC1>rHZ~YY_ zGx?n7ehxGvz@&4e)1KMF6mP_yr;XwE;?eDbE}7Uj&{dk;#2u$i35PL@u9`R45vY{( zV(HH;$9mUb)vb936lU#O9};GuFrqoS{pype zmc#vf7u6*bSP+g;LkE8;^sqVcpNm~B!fj*qc=ywo;J4T&sO}imaiz!ruf5c|5D4tZ zs0u0DjFvYQfq)~rpF4bcei zxau1UAerRbO%G$nuP(+h>Hym+RY~0u`UZueUnnzvl{Z%W?VJaVKronmh>x}IIYPBSnYa<8#}G_k(QB>2$&4=cLnKX zSUxtKbAn+2Q`aHn@TdibQ`?@{}EtlNb(8veApH0mAtZ}}Ex z0v}H+hm2URSn#YKe@Y%wct5Sm6`*wL0JFCSnH4)sXcIXhS_!FXmGIMq2Rn2)S$X([e5cv#Li9(W@g$tf8}Zn%8+)sq%&nvk>Anp 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 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_hrs_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_hrs_feature.png deleted file mode 100644 index 3f255d0b709398304e17cb452c5082b2a24f8ef9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9037 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 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_hts_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_hts_feature.png deleted file mode 100644 index b79f88ce081e1ad106ac55b1eda06edcd46044af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7235 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 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_rsc_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_rsc_feature.png deleted file mode 100644 index 4bd5b595aeccb1d4185ceac1719ec06428173c76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11880 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 FV1gn`ufhNT diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_silent.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_proximity_silent.png deleted file mode 100644 index 419d1ef147ca9514df4636db0b787fa164d51961..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1449 zcmV;a1y=frP)hn6grAVnryLm{^PY0+BR6XwbCONFU^ZCTp6e6i_f~ z7g{9ImpD4lAQ&0G|m;OBD}|Dsz>u!Zxy46B~+II`QwtDLKbxY4`t zN%CDAt3tlVRCRy_>cih=x|bkuDf_5GeO#h8{JI(LEfBbzm#Ip>aGA>RRyRf2&aE)fATigNw)OpSZV7q#N4=k#3ix2_; zK41gTp&sD0Wo2T7C_7mS09?f*jGETh4T~-*zzOvLS0vSm5Ejh1dYJvD^=<%QnH1oX zdVonub^Z2JFjkl;uVKyySS(%BjW~_Vd3RMP`R+X8wgb*7^cWu(pd7#?ewLUQISnh% zF~HwKgcImRF3#d6z(=){QV(z9Ipe2ThOaS&!}tL)RARlrHaA`GKsa$6W`xp_-7M9Z za8Eqh9#`LlMjhDHp^a+t-J|N8kl1A+{ z)B~)P4m{Hnggxf<>jDqu#&^2_fNP}w)~E-_Ntg~K>!@KhV&2)G+>p4ITNb0Dz%{<-Oot;Pb}(s8s6fm^XU{FFQ#La!UfjL5tV9kUvU&)q4Z5*jQm% zsl~7hB{ZZ%A_VTWnM~j7)xOO)HWk;Hmb*D@EGY#Dd{OGUnf^d+Zvl;+c%^nU4-vv=xxB}jaKe^3vn#o-!2z_qjai2hX%Y|($;V-Zi9kUU% zvz=2wbaRTia)ZBN>js5{_p&LIau62+VUewF@Ldxy?E` z0}%EpI%wk;0t-_fm3>E8pL9$x;}G7SQ@9rcgFu}ZT!(+2g~F7*PY4fk%2oI`83L#U z31O!zfE`9DjqL$JU=EKv4*#p$r%{50up}X=E7SFvXLN&v@T7wdHO?oJ$^{5vuPs2U zu$;z@Kp-%WC*`M5tTI0q2<_>A1||I6GW_4TSW+?>Am+` diff --git a/app/src/main/res/drawable-xxxhdpi/ic_template_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_template_feature.png deleted file mode 100644 index d742a85cd33d10e337de8b0f0ac4aed9cc209292..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9210 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 a#WIa3IKPw64Q@{l1bxBge1#eA?*9Rt*t}N& diff --git a/app/src/main/res/drawable-xxxhdpi/ic_uart_feature.png b/app/src/main/res/drawable-xxxhdpi/ic_uart_feature.png deleted file mode 100644 index d49e65c79eca4e782133b757b856897bc59cf5f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8602 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 diff --git a/app/src/main/res/drawable/app_file_browser.xml b/app/src/main/res/drawable/app_file_browser.xml deleted file mode 100644 index 09b92388..00000000 --- a/app/src/main/res/drawable/app_file_browser.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_action_add.xml b/app/src/main/res/drawable/ic_action_add.xml deleted file mode 100644 index b5f0f258..00000000 --- a/app/src/main/res/drawable/ic_action_add.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_action_clear.xml b/app/src/main/res/drawable/ic_action_clear.xml deleted file mode 100644 index 88047bc2..00000000 --- a/app/src/main/res/drawable/ic_action_clear.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_action_download.xml b/app/src/main/res/drawable/ic_action_download.xml deleted file mode 100644 index e0953c2b..00000000 --- a/app/src/main/res/drawable/ic_action_download.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml deleted file mode 100644 index afde3102..00000000 --- a/app/src/main/res/drawable/ic_battery.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_feature_bg.xml b/app/src/main/res/drawable/ic_feature_bg.xml deleted file mode 100644 index b06fc4da..00000000 --- a/app/src/main/res/drawable/ic_feature_bg.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_feature_bg_n.xml b/app/src/main/res/drawable/ic_feature_bg_n.xml deleted file mode 100644 index 35242a5d..00000000 --- a/app/src/main/res/drawable/ic_feature_bg_n.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_feature_bg_p.xml b/app/src/main/res/drawable/ic_feature_bg_p.xml deleted file mode 100644 index 9a567f48..00000000 --- a/app/src/main/res/drawable/ic_feature_bg_p.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_feature_small_bg.xml b/app/src/main/res/drawable/ic_feature_small_bg.xml deleted file mode 100644 index 8134d3db..00000000 --- a/app/src/main/res/drawable/ic_feature_small_bg.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_feature_small_bg_n.xml b/app/src/main/res/drawable/ic_feature_small_bg_n.xml deleted file mode 100644 index 61377059..00000000 --- a/app/src/main/res/drawable/ic_feature_small_bg_n.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_feature_small_bg_p.xml b/app/src/main/res/drawable/ic_feature_small_bg_p.xml deleted file mode 100644 index 3361815a..00000000 --- a/app/src/main/res/drawable/ic_feature_small_bg_p.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_nrf_connect_feature_small.xml b/app/src/main/res/drawable/ic_nrf_connect_feature_small.xml deleted file mode 100644 index 60e5eed0..00000000 --- a/app/src/main/res/drawable/ic_nrf_connect_feature_small.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_rssi_bar.xml b/app/src/main/res/drawable/ic_rssi_bar.xml deleted file mode 100644 index e37a8b74..00000000 --- a/app/src/main/res/drawable/ic_rssi_bar.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/nordic_logo.xml b/app/src/main/res/drawable/nordic_logo.xml deleted file mode 100644 index 14679a6e..00000000 --- a/app/src/main/res/drawable/nordic_logo.xml +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/nordic_logo_horiz_white.xml b/app/src/main/res/drawable/nordic_logo_horiz_white.xml deleted file mode 100644 index af916231..00000000 --- a/app/src/main/res/drawable/nordic_logo_horiz_white.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/start_edit_mode.xml b/app/src/main/res/drawable/start_edit_mode.xml deleted file mode 100644 index de9d6192..00000000 --- a/app/src/main/res/drawable/start_edit_mode.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/stop_edit_mode.xml b/app/src/main/res/drawable/stop_edit_mode.xml deleted file mode 100644 index d5491ef8..00000000 --- a/app/src/main/res/drawable/stop_edit_mode.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/uart_button.xml b/app/src/main/res/drawable/uart_button.xml deleted file mode 100644 index 0abfeb58..00000000 --- a/app/src/main/res/drawable/uart_button.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/uart_button_background.xml b/app/src/main/res/drawable/uart_button_background.xml deleted file mode 100644 index 3c1580e6..00000000 --- a/app/src/main/res/drawable/uart_button_background.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/uart_button_small.xml b/app/src/main/res/drawable/uart_button_small.xml deleted file mode 100644 index fb981d2d..00000000 --- a/app/src/main/res/drawable/uart_button_small.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/uart_dialog_button_background.xml b/app/src/main/res/drawable/uart_dialog_button_background.xml deleted file mode 100644 index fd4991fb..00000000 --- a/app/src/main/res/drawable/uart_dialog_button_background.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_feature_bpm.xml b/app/src/main/res/layout-land/activity_feature_bpm.xml deleted file mode 100644 index b03b233d..00000000 --- a/app/src/main/res/layout-land/activity_feature_bpm.xml +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-land/activity_feature_cgms.xml b/app/src/main/res/layout-land/activity_feature_cgms.xml deleted file mode 100644 index ccb5a46d..00000000 --- a/app/src/main/res/layout-land/activity_feature_cgms.xml +++ /dev/null @@ -1,220 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -