From b37fe7f3309abc14fb8ecd0e83386621e27bbfe8 Mon Sep 17 00:00:00 2001 From: John Zhou <37914490+johnnzhou@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:15:14 -0700 Subject: [PATCH] Reimplement the measurement app in Kotlin (#13) Co-authored-by: achintiii Co-authored-by: Matt Johnson Co-authored-by: Sudheesh Singanamalla --- .github/workflows/android.yml | 48 ++ .gitignore | 5 +- README.md | 96 +++- app/build.gradle | 167 ++++++- app/proguard-rules.pro | 69 ++- .../ExampleInstrumentedTest.java | 52 +- app/src/main/AndroidManifest.xml | 72 ++- app/src/main/java/README.md | 83 ++++ .../Functionality/Iperf.java | 9 - .../Functionality/Ping.java | 10 - .../lcl/lclmeasurementtool/LCLApplication.kt | 23 + .../lcl/lclmeasurementtool/MainActivity.java | 206 -------- .../lcl/lclmeasurementtool/MainActivity2.kt | 158 ++++++ .../MainActivityViewModel.kt | 470 ++++++++++++++++++ .../Managers/CellularChangeListener.java | 16 - .../Managers/CellularManager.java | 159 ------ .../Managers/LocationServiceListener.java | 105 ---- .../Managers/LocationServiceManager.java | 103 ---- .../Managers/NetworkChangeListener.java | 29 -- .../Managers/NetworkManager.java | 169 ------- .../Utils/AbstractDataTransferRate.java | 19 - .../Utils/ConvertUtils.java | 40 -- .../Utils/DataTransferRateUnit.java | 141 ------ .../Utils/SignalStrengthLevel.java | 111 ----- .../lcl/lclmeasurementtool/Utils/UIUtils.java | 51 -- .../lclmeasurementtool/Utils/UnitUtils.java | 13 - .../constants/IperfConstants.kt | 42 ++ .../constants/NetworkConstants.kt | 23 + .../lclmeasurementtool/constants/README.md | 3 + .../constants/SimCardConstants.kt | 17 + .../lcl/lclmeasurementtool/database/README.md | 8 + .../database/dao/ConnectivityDao.kt | 30 ++ .../database/dao/SignalStrengthDao.kt | 31 ++ .../database/db/AppDatabase.kt | 31 ++ .../ConnectivityMonitorDataSource.kt | 73 +++ .../datasource/LocationDataSource.kt | 68 +++ .../datasource/PreferencesDataSource.kt | 86 ++++ .../datasource/SignalStrengthDataSource.kt | 91 ++++ .../datasource/SimStateMonitorDataSource.kt | 68 +++ .../datastore/DataStoreModule.kt | 35 ++ .../datastore/Dispatcher.kt | 12 + .../datastore/DispatcherModule.kt | 16 + .../datastore/UserPreferencesSerializer.kt | 27 + .../errors/DecoderException.java | 82 +++ .../errors/EncoderException.java | 86 ++++ .../features/mlab/MLabCallback.kt | 10 + .../features/mlab/MLabResult.kt | 16 + .../features/mlab/MLabRunner.kt | 84 ++++ .../lclmeasurementtool/features/ping/Ping.kt | 47 ++ .../features/ping/PingError.kt | 14 + .../features/ping/PingResult.kt | 10 + .../features/ping/PingUtil.kt | 75 +++ .../location/LocationService.kt | 9 + .../model/datamodel/BaseMeasureDataModel.kt | 16 + .../datamodel/ConnectivityReportModel.kt | 22 + .../model/datamodel/MeasurementReportModel.kt | 10 + .../model/datamodel/QRCodeKeysModel.kt | 9 + .../model/datamodel/RegistrationModel.kt | 9 + .../datamodel/SignalStrengthReportModel.kt | 20 + .../model/datamodel/UserData.kt | 12 + .../repository/ConnectivityRepository.kt | 49 ++ .../model/repository/HistoryDataRepository.kt | 10 + .../model/repository/LCLApiRepository.kt | 15 + .../repository/LocalUserDataRepository.kt | 21 + .../model/repository/NetworkApiRepository.kt | 11 + .../repository/SignalStrengthRepository.kt | 48 ++ .../model/repository/UserDataRepository.kt | 14 + .../model/viewmodels/ConnectivityViewModel.kt | 42 ++ .../model/viewmodels/SettingsViewModel.kt | 41 ++ .../viewmodels/SignalStrengthViewModel.kt | 41 ++ .../lclmeasurementtool/modules/DaosModule.kt | 22 + .../lclmeasurementtool/modules/DataModule.kt | 61 +++ .../modules/DatabaseModule.kt | 25 + .../modules/NetworkModule.kt | 17 + .../networking/NetworkAPI.kt | 44 ++ .../networking/NetworkMonitor.kt | 7 + .../networking/SimStateMonitor.kt | 7 + .../sync/DelegatingWorker.kt | 64 +++ .../sync/SyncInitializer.kt | 40 ++ .../lclmeasurementtool/sync/UploadWorker.kt | 70 +++ .../telephony/SignalStrengthLevel.kt | 49 ++ .../telephony/SignalStrengthMonitor.kt | 9 + .../lcl/lclmeasurementtool/ui/AlertDialog.kt | 43 ++ .../com/lcl/lclmeasurementtool/ui/AppState.kt | 98 ++++ .../lclmeasurementtool/ui/ConnectivityItem.kt | 105 ++++ .../lcl/lclmeasurementtool/ui/HistoryItem.kt | 6 + .../lclmeasurementtool/ui/HistoryScreen.kt | 153 ++++++ .../lcl/lclmeasurementtool/ui/HomeScreen.kt | 256 ++++++++++ .../lclmeasurementtool/ui/LCLApplication.kt | 203 ++++++++ .../com/lcl/lclmeasurementtool/ui/LCLIcons.kt | 25 + .../lclmeasurementtool/ui/LCLLloadingWheel.kt | 106 ++++ .../com/lcl/lclmeasurementtool/ui/Login.kt | 92 ++++ .../lcl/lclmeasurementtool/ui/SettingsView.kt | 230 +++++++++ .../ui/SignalStrengthItem.kt | 109 ++++ .../com/lcl/lclmeasurementtool/ui/TagIcon.kt | 56 +++ .../ui/navigation/HistoryNavigation.kt | 26 + .../ui/navigation/HomeNavigation.kt | 20 + .../ui/navigation/TopLevelDestination.kt | 26 + .../lclmeasurementtool/util/AnalyticsUtil.kt | 14 + .../com/lcl/lclmeasurementtool/util/ECDSA.kt | 195 ++++++++ .../com/lcl/lclmeasurementtool/util/Hex.java | 442 ++++++++++++++++ .../lclmeasurementtool/util/NetworkUtil.kt | 14 + .../lclmeasurementtool/util/SecurityUtil.kt | 32 ++ .../lcl/lclmeasurementtool/util/SyncUtil.kt | 35 ++ .../com/lcl/lclmeasurementtool/util/Time.kt | 10 + app/src/main/proto/user_preferences.proto | 13 + .../drawable-v24/ic_launcher_foreground.xml | 30 -- .../main/res/drawable/cellular_strength.xml | 9 - app/src/main/res/drawable/download.xml | 9 - .../res/drawable/ic_launcher_background.xml | 170 ------- .../main/res/drawable/lcl_purple_gold_uw.png | Bin 0 -> 88097 bytes app/src/main/res/drawable/ping.xml | 9 - .../drawable/signal_strength_indicator.xml | 14 - app/src/main/res/drawable/start.xml | 5 - app/src/main/res/drawable/stop.xml | 5 - app/src/main/res/drawable/upload.xml | 9 - app/src/main/res/layout/activity_main.xml | 223 --------- app/src/main/res/layout/stats_template.xml | 41 -- app/src/main/res/menu/menu_main.xml | 10 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3593 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5339 -> 0 bytes app/src/main/res/mipmap-hdpi/icon.png | Bin 0 -> 9626 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2636 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3388 -> 0 bytes app/src/main/res/mipmap-mdpi/icon.png | Bin 0 -> 5248 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4926 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7472 -> 0 bytes app/src/main/res/mipmap-xhdpi/icon.png | Bin 0 -> 14979 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7909 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 11873 -> 0 bytes app/src/main/res/mipmap-xxhdpi/icon.png | Bin 0 -> 26605 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10652 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 16570 -> 0 bytes app/src/main/res/mipmap-xxxhdpi/icon.png | Bin 0 -> 39020 bytes app/src/main/res/values-land/dimens.xml | 3 + app/src/main/res/values-w1240dp/dimens.xml | 3 + app/src/main/res/values-w600dp/dimens.xml | 3 + app/src/main/res/values/arrays.xml | 12 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 76 ++- app/src/main/res/values/themes.xml | 13 +- app/src/main/res/xml/filepaths.xml | 4 + .../ConvertUtilUnitTest.java | 72 --- .../com/lcl/lclmeasurementtool/ECDSATest.java | 70 +++ build.gradle | 15 +- docs/img/android_sdk.png | Bin 0 -> 412626 bytes docs/img/android_sdk_tools.png | Bin 0 -> 304575 bytes docs/img/androidstudio_tool_bar.png | Bin 0 -> 16066 bytes docs/img/device_manager.png | Bin 0 -> 91976 bytes docs/img/sdk_ndk_emulator.png | Bin 0 -> 6222 bytes gradle.properties | 3 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- libndt7/.gitignore | 1 + libndt7/build.gradle | 63 +++ libndt7/proguard-rules.pro | 38 ++ .../ndt7/androidTest/NDT7HealthTest.kt | 102 ++++ .../androidTest/PayloadTransformerTest.kt | 39 ++ libndt7/src/main/AndroidManifest.xml | 10 + .../ndt7/android/DataPublisher.kt | 22 + .../measurementlab/ndt7/android/Downloader.kt | 98 ++++ .../measurementlab/ndt7/android/NDTTest.kt | 129 +++++ .../measurementlab/ndt7/android/Uploader.kt | 113 +++++ .../ndt7/android/models/CallbackRegistry.kt | 11 + .../ndt7/android/models/ClientResponse.kt | 17 + .../ndt7/android/models/HostnameResponse.kt | 41 ++ .../ndt7/android/models/Measurement.kt | 85 ++++ .../ndt7/android/utils/DataConverter.kt | 25 + .../ndt7/android/utils/HttpClientFactory.kt | 16 + .../ndt7/android/utils/NDT7Constants.kt | 13 + .../ndt7/android/utils/PayloadTransformer.kt | 18 + .../ndt7/android/utils/SocketFactory.kt | 22 + libndt7/src/main/res/values/strings.xml | 3 + settings.gradle | 1 + 176 files changed, 6452 insertions(+), 1871 deletions(-) create mode 100644 .github/workflows/android.yml create mode 100644 app/src/main/java/README.md delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Iperf.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Ping.java create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/LCLApplication.kt delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/MainActivity.java create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularChangeListener.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularManager.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceListener.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceManager.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkChangeListener.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkManager.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Utils/AbstractDataTransferRate.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Utils/ConvertUtils.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Utils/DataTransferRateUnit.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Utils/SignalStrengthLevel.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Utils/UIUtils.java delete mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/Utils/UnitUtils.java create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/constants/IperfConstants.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/constants/NetworkConstants.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/constants/README.md create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/constants/SimCardConstants.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/database/README.md create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/database/dao/ConnectivityDao.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/database/dao/SignalStrengthDao.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/database/db/AppDatabase.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datasource/ConnectivityMonitorDataSource.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datasource/LocationDataSource.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datasource/PreferencesDataSource.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datasource/SignalStrengthDataSource.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datasource/SimStateMonitorDataSource.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datastore/DataStoreModule.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datastore/Dispatcher.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datastore/DispatcherModule.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/datastore/UserPreferencesSerializer.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/errors/DecoderException.java create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/errors/EncoderException.java create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/features/ping/Ping.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingError.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingResult.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingUtil.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/location/LocationService.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/BaseMeasureDataModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/ConnectivityReportModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/MeasurementReportModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/QRCodeKeysModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/RegistrationModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/SignalStrengthReportModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/UserData.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/repository/ConnectivityRepository.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/repository/HistoryDataRepository.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LCLApiRepository.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LocalUserDataRepository.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/repository/NetworkApiRepository.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/repository/SignalStrengthRepository.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/repository/UserDataRepository.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/ConnectivityViewModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SettingsViewModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SignalStrengthViewModel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/modules/DaosModule.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/modules/DataModule.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/modules/DatabaseModule.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/modules/NetworkModule.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkAPI.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkMonitor.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/networking/SimStateMonitor.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/sync/DelegatingWorker.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/sync/SyncInitializer.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/sync/UploadWorker.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthLevel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthMonitor.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/AlertDialog.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/AppState.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/ConnectivityItem.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryItem.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryScreen.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLApplication.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLIcons.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLLloadingWheel.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/Login.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/SignalStrengthItem.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/TagIcon.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HistoryNavigation.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HomeNavigation.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/TopLevelDestination.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/AnalyticsUtil.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/ECDSA.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/Hex.java create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/NetworkUtil.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/SecurityUtil.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/SyncUtil.kt create mode 100644 app/src/main/java/com/lcl/lclmeasurementtool/util/Time.kt create mode 100644 app/src/main/proto/user_preferences.proto delete mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 app/src/main/res/drawable/cellular_strength.xml delete mode 100644 app/src/main/res/drawable/download.xml delete mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/lcl_purple_gold_uw.png delete mode 100644 app/src/main/res/drawable/ping.xml delete mode 100644 app/src/main/res/drawable/signal_strength_indicator.xml delete mode 100644 app/src/main/res/drawable/start.xml delete mode 100644 app/src/main/res/drawable/stop.xml delete mode 100644 app/src/main/res/drawable/upload.xml delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/layout/stats_template.xml delete mode 100644 app/src/main/res/menu/menu_main.xml delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-hdpi/icon.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/icon.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/icon.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/icon.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/icon.png create mode 100644 app/src/main/res/values-land/dimens.xml create mode 100644 app/src/main/res/values-w1240dp/dimens.xml create mode 100644 app/src/main/res/values-w600dp/dimens.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/xml/filepaths.xml delete mode 100644 app/src/test/java/com/lcl/lclmeasurementtool/ConvertUtilUnitTest.java create mode 100644 app/src/test/java/com/lcl/lclmeasurementtool/ECDSATest.java create mode 100644 docs/img/android_sdk.png create mode 100644 docs/img/android_sdk_tools.png create mode 100644 docs/img/androidstudio_tool_bar.png create mode 100644 docs/img/device_manager.png create mode 100644 docs/img/sdk_ndk_emulator.png create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 libndt7/.gitignore create mode 100644 libndt7/build.gradle create mode 100644 libndt7/proguard-rules.pro create mode 100644 libndt7/src/androidTest/java/net/measurementlab/ndt7/androidTest/NDT7HealthTest.kt create mode 100644 libndt7/src/androidTest/java/net/measurementlab/ndt7/androidTest/PayloadTransformerTest.kt create mode 100644 libndt7/src/main/AndroidManifest.xml create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/DataPublisher.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/Downloader.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/NDTTest.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/Uploader.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/models/CallbackRegistry.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/models/ClientResponse.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/models/HostnameResponse.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/models/Measurement.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/utils/DataConverter.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/utils/HttpClientFactory.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/utils/NDT7Constants.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/utils/PayloadTransformer.kt create mode 100644 libndt7/src/main/java/net/measurementlab/ndt7/android/utils/SocketFactory.kt create mode 100644 libndt7/src/main/res/values/strings.xml diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..16df6f7 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,48 @@ +name: LCL Measurement Tool CI + +env: + # The name of the main module repository + main_project_module: app + +on: + push: + branches: [main] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: gradle + - name: Setup Android SDK + uses: android-actions/setup-android@v2 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew app:build + - name: Run Tests + run: ./gradlew app:testDemoDebugUnitTest + - name: Generate APK Debug Build with Gradle (Dev) + if: startsWith(github.ref, 'refs/tags/') + run: ./gradlew assembleDevDebug + - name: Generate APK Release Build with Gradle (Full) + if: startsWith(github.ref, 'refs/tags/') + run: ./gradlew assembleFullRelease + - name: Upload to Github Page + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + files: | + ${{ env.main_project_module }}/build/outputs/apk/dev/debug/app-dev-debug.apk + ${{ env.main_project_module }}/build/outputs/apk/full/debug/app-full-release.apk + draft: true + prerelease: true diff --git a/.gitignore b/.gitignore index 70e9b81..aa0cf03 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ .mtj.tmp/ # Package Files # -*.jar +#*.jar *.war *.nar *.ear @@ -54,3 +54,6 @@ captures/ .idea # Keystore files *.jks +*.properties +# External native build objects +.cxx/ diff --git a/README.md b/README.md index b6c7e72..89c6800 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,97 @@ # LCL Network Performance Measurement Tool -LCL Network Measurement Tool is an Android App that measures a variety of network metrics, including but not limited to *ping*, *upload/download* speed, *signal strength*. +**Warning: This doc is outdated! Will update once the kotlin integration is complete.** + +**LCL Network Measurement Tool** is an Android App that measures a variety of network metrics, including but not limited to *ping*, *upload/download* speed, *signal strength*. +It also automatically uploads the testing results to the backend server, which will then be displayed on [our coverage map](https://coverage.seattlecommunitynetwork.org/). + +## Project Structure +``` +app/src/main/java +└── com + └── lcl + └── lclmeasurementtool + ├── Utils + ├── Receivers + ├── Models + ├── Managers + ├── Functionality + ├── Database + │   ├── Entity + │   └── DB + └── Constants +``` + +In each sub-directory, there is a README file explaining the module. + + +## Building, Running, and Installing the project +### Prerequisite +* We use Android Studio for development. Download the latest version of Android Studio [here](https://developer.android.com/studio). + * Android SDK 30 (make sure you have SDK version 30 or above) + * Android NDK (Download the latest NDK either from Android's website or in Android Studio) + * Android Emulator +* Git + +### Managing SDK, NDK, and Emulators +Android Studio has tools to help manage the SDK, NDK, and Emulators resources. + +![SDK, NDK, Emulator Manager](docs/img/sdk_ndk_emulator.png) + +#### SDK and NDK +Android SDK is a component for building apps on Android platform. +Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++. + +To properly configure SDK and NDK for the project. First go to the `SDK Platform` pane, and check the SDK with API level equal to or greater than 30. +![Android SDK](docs/img/android_sdk.png) + +Then click `SDK Tools`, make sure at least NDK and CMake are checked. +![Android SDK Tools](docs/img/android_sdk_tools.png) + +Finally, click `OK` and wait for these tools to be installed. + +#### Emulators +If you do not have a Andriod Phone, you can install emulators to run the app. Click the `Device Manager` button in the middle. Then click `Create device`. +Select a phone target (we recommend using Pixel 4). Then select your system image, which should match your API level. Finally click `Finish` and wait for the emulator to be created. +![Android Device Manager](docs/img/device_manager.png) + +For more information, visit [Android Developer Website](https://developer.android.com/studio/releases/platforms) + +### Building, Running and Debugging the Code +Android Studio provides out-of-box build tool and debugger to help build, run, and debug. + +#### Open the project +* In order to build the code, clone this project repo to your working folder + * using HTTPS: `$ git clone https://github.com/Local-Connectivity-Lab/lcl_measurement_tool.git` + * using SSH if you have set it up: `$ git@github.com:Local-Connectivity-Lab/lcl_measurement_tool.git` +* `cd` into the directory `lcl_measurement_tool` and change the branch from `main` to `develop` to view the latest codebase `$ git checkout dev` +* Open the project using Android Studio. Wait for Android Studio to automatically install Gradle and other dependencies used by the project. +* Under the top-level directory of this project, where this README.md lives, create a new file `iperfkey.properties` and ask either Esther Jang or Zhennan Zhou for the keys. + +#### Building the project +* On the top menu bar, find the `Build -> Clean Project` to remove any previous build. Then `Build -> Rebuild Project`. +* [Optional] On the bottom menu bar, find the Build Panel and view the Build process. + +#### Running/Debugging the project +After building the project, we can use Android Studio's "Play" button in the toolbar to run the project on an actual Android phone or in the Emulator of your choice. +If you need to debug the code, add breakpoints and then click the `bug` button in the toolbar. Android Studio will pop up a debugger attached to the process. +![AndroidStudio toolbar](docs/img/androidstudio_tool_bar.png) Make sure the target shows as `app`. + +#### Profiling and Inspection +If you need to inspect the network packet or checking the app's running state, you can use Android Studio's built-in profiler located at the bottom toolbar. +Make sure you open the profiler when the app is running. To inspect the database, using the App Inspection tool located at the bottom toolbar. + + +## Contributing to the Project +We are excited to work alongside you, our amazing community, to build and enhance this measurement tool! If you want to contribute to this project, contact Esther Jang(infared) or Zhennan Zhou(Johnnnzhou) in our Discord channel. + +### Getting involved with Seattle Community Networks Team +If you are interested in contributing to the community, visit our [website](https://seattlecommunitynetwork.org/), join our [Discord channel](https://discord.gg/sZkK5RpeCE), +and follow us on social media: [Instagram](https://instagram.com/seattlecommnet), [Facebook](https://facebook.com/seattlecommnet), and [Twitter](https://twitter.com/seattlecommnet). + +For more information, visit our [Get Started Pro Tips](https://docs.seattlecommunitynetwork.org/get-started.html). + + + + + diff --git a/app/build.gradle b/app/build.gradle index f8270d3..a7db696 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,47 +1,178 @@ plugins { id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'kotlin-android' + id 'com.google.dagger.hilt.android' + id "com.google.protobuf" version "0.8.17" + id "kotlinx-serialization" } android { - compileSdkVersion 30 + compileSdkVersion 33 buildToolsVersion "30.0.3" - + ndkVersion "23.1.7779620" defaultConfig { applicationId "com.lcl.lclmeasurementtool" - minSdkVersion 29 - targetSdkVersion 30 - versionCode 1 - versionName "1.0" - + minSdkVersion 24 + targetSdkVersion 31 + versionCode 2 + versionName "1.0.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + + // Required when setting minSdkVersion to 20 or lower according to https://developer.android.com/studio/write/java8-support#library-desugaring + multiDexEnabled true + } + + flavorDimensions "mode" + productFlavors { + + dev { + dimension "mode" + versionNameSuffix "-dev" + } + + demo { + dimension "mode" + versionNameSuffix "-demo" + } + + full { + dimension "mode" + versionNameSuffix "-full" + } } buildTypes { release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + debug { + // below will add the suffix to the *com.lcl.lclmeasurementtool* => **com.lcl.lclmeasurementtool.debug** +// applicationIdSuffix ".debug" minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + buildFeatures { viewBinding true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion "1.4.4" + } + + kotlinOptions { + jvmTarget = '1.8' } } dependencies { + implementation project(":libndt7") - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.2.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.1' - implementation 'androidx.navigation:navigation-fragment:2.3.0' - implementation 'androidx.navigation:navigation-ui:2.3.0' - implementation 'com.google.android.gms:play-services-location:18.0.0' - implementation 'com.android.volley:volley:1.2.0' - implementation 'com.google.android.gms:play-services-location:18.0.0' +// implementation fileTree(dir: 'libs', include: ['*.jar']) + + // Kotlin + implementation 'androidx.core:core-ktx:1.10.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0" + + // Worker + implementation 'androidx.work:work-runtime-ktx:2.8.1' + implementation "androidx.hilt:hilt-work:1.0.0" + kapt "androidx.hilt:hilt-compiler:1.0.0" + + // Location Service + implementation 'com.google.android.gms:play-services-location:21.0.1' + + // DB + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + + // UI + implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' + implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' + def appCenterSdkVersion = '4.1.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' + def composeBom = platform('androidx.compose:compose-bom:2022.10.00') + implementation composeBom + androidTestImplementation composeBom + implementation "androidx.compose.material3:material3:1.0.1" + implementation "androidx.compose.material3:material3-window-size-class:1.0.1" + implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.1" + debugImplementation "androidx.compose.ui:ui-tooling-preview:1.3.2" + implementation "androidx.compose.ui:ui-tooling:1.3.2" + implementation "androidx.compose.material:material-icons-extended:1.3.1" + implementation "androidx.navigation:navigation-compose:2.5.3" + + // datastore with protobuf preferences + implementation "androidx.datastore:datastore-preferences:1.0.0" + implementation "androidx.datastore:datastore:1.0.0" + implementation "com.google.protobuf:protobuf-javalite:3.21.12" + + // hilt dependency injection + implementation 'com.google.dagger:hilt-android:2.44.2' + implementation 'androidx.hilt:hilt-navigation-compose:1.0.0' + kapt 'com.google.dagger:hilt-compiler:2.44.2' + + // start up runtime + implementation "androidx.startup:startup-runtime:1.1.1" + + // third-party lib + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' + implementation "com.squareup.okhttp3:okhttp:$okhttp_version" + implementation 'com.github.kongzue.DialogX:DialogX:0.0.43.beta13' + implementation 'com.github.getActivity:XXPermissions:16.6' + implementation "io.github.g00fy2.quickie:quickie-bundled:1.6.0" + implementation 'org.bouncycastle:bcpkix-jdk15to18:1.70' + implementation 'org.bouncycastle:bcprov-jdk15to18:1.70' + implementation 'org.apache.commons:commons-csv:1.9.0' + implementation 'io.github.azhon:appupdate:4.3.2' + + // Analytics + implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}" + implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}" + + // Testing testImplementation 'junit:junit:4.+' - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' -} \ No newline at end of file +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.14.0" + } + + // Generates the java Protobuf-lite code for the Protobufs in this project. See + // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation + // for more information. + generateProtoTasks { + all().each { task -> + task.builtins { + java { + option("lite") + } + } + } + } +} + +kapt { + correctErrorTypes true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..067f2d8 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,71 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile +-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite { + ; +} + +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} + +# Ignore annotation used for build tooling. +#-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +# Keep inherited services. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface * extends <1> + +# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). +-keep,allowobfuscation,allowshrinking interface retrofit2.Call +-keep,allowobfuscation,allowshrinking class retrofit2.Response + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# A resource is loaded with a relative path so the package of this class must be preserved. +-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* \ No newline at end of file diff --git a/app/src/androidTest/java/com/lcl/lclmeasurementtool/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/lcl/lclmeasurementtool/ExampleInstrumentedTest.java index 21008d6..42f6393 100644 --- a/app/src/androidTest/java/com/lcl/lclmeasurementtool/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/com/lcl/lclmeasurementtool/ExampleInstrumentedTest.java @@ -1,26 +1,26 @@ -package com.lcl.lclmeasurementtool; - -import android.content.Context; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.lcl.lclmeasurementtool", appContext.getPackageName()); - } -} \ No newline at end of file +//package com.lcl.lclmeasurementtool; +// +//import android.content.Context; +// +//import androidx.test.platform.app.InstrumentationRegistry; +//import androidx.test.ext.junit.runners.AndroidJUnit4; +// +//import org.junit.Test; +//import org.junit.runner.RunWith; +// +//import static org.junit.Assert.*; +// +///** +// * Instrumented test, which will execute on an Android device. +// * +// * @see Testing documentation +// */ +//@RunWith(AndroidJUnit4.class) +//public class ExampleInstrumentedTest { +// @Test +// public void useAppContext() { +// // Context of the app under test. +// Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); +// assertEquals("com.lcl.lclmeasurementtool", appContext.getPackageName()); +// } +//} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e10f88..7d39113 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,22 +1,77 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + android:theme="@style/Theme.LCLMeasurementTool" + android:name=".LCLApplication" + android:requestLegacyExternalStorage="true" + > + + + + + + + + + + @@ -25,4 +80,5 @@ + \ No newline at end of file diff --git a/app/src/main/java/README.md b/app/src/main/java/README.md new file mode 100644 index 0000000..af5957e --- /dev/null +++ b/app/src/main/java/README.md @@ -0,0 +1,83 @@ +# LCL Measurement Tool Main Codebase + +## Main Structure +``` +└── lclmeasurementtool + ├── ConnectivityDataFragment.java + ├── Constants + │   ├── IperfConstants.java + │   ├── NetworkConstants.java + │   └── SimCardConstants.java + ├── Database + │   ├── DB + │   │   └── MeasurementResultDatabase.java + │   └── Entity + │   ├── AbstractViewModel.java + │   ├── Connectivity.java + │   ├── ConnectivityDAO.java + │   ├── ConnectivityViewModel.java + │   ├── DataEncodable.java + │   ├── EntityEnum.java + │   ├── SignalStrength.java + │   ├── SignalStrengthDAO.java + │   └── SignalViewModel.java + ├── Functionality + │   ├── AbstractIperfWorker.java + │   ├── AbstractPingWorker.java + │   ├── Iperf3Callback.java + │   ├── Iperf3Client.java + │   ├── Iperf3Config.java + │   ├── IperfDownStreamWorker.java + │   ├── IperfUpStreamWorker.java + │   ├── NetworkTestViewModel.java + │   ├── Ping.java + │   ├── PingError.java + │   ├── PingListener.java + │   ├── PingStats.java + │   ├── PingUtils.java + │   └── PingWorker.java + ├── HomeFragment.java + ├── MainActivity.java + ├── Managers + │   ├── CellularChangeListener.java + │   ├── CellularManager.java + │   ├── KeyStoreManager.java + │   ├── LocationServiceListener.java + │   ├── LocationServiceManager.java + │   ├── LocationUpdatesListener.java + │   ├── NetworkChangeListener.java + │   ├── NetworkManager.java + │   └── UploadManager.java + ├── Models + │   ├── ConnectivityMessageModel.java + │   ├── MeasurementDataModel.java + │   ├── MeasurementDataReportModel.java + │   ├── QRCodeKeysModel.java + │   ├── RegistrationMessageModel.java + │   └── SignalStrengthMessageModel.java + ├── Receivers + │   └── SimStatesReceiver.java + ├── SettingsFragment.java + ├── SignalDataFragment.java + └── Utils + ├── AbstractDataTransferRate.java + ├── AnalyticsUtils.java + ├── ConvertUtils.java + ├── DataTransferRateUnit.java + ├── DecoderException.java + ├── ECDSA.java + ├── EncoderException.java + ├── Hex.java + ├── LocationUtils.java + ├── SecurityUtils.java + ├── SerializationUtils.java + ├── SignalStrengthLevel.java + ├── TimeUtils.java + ├── UIUtils.java + └── UnitUtils.java + +``` +`ConnectivityDataFragment.java`, `HomeFragment.java`, `MainActivity.java`, `SettingsFragment.java`, `SignalDataFragment.java` +are major UI views and controllers for the measurement app. In particular, `MainActivity.java` is the entry point of the app. + +For more detailed info, check out the readme file in each sub-directories. \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Iperf.java b/app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Iperf.java deleted file mode 100644 index 6c21ed6..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Iperf.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.lcl.lclmeasurementtool.Functionality; - -/** - * Iperf a functionality module that is able to - * test the upload and download speed from the current device. - */ -public class Iperf { - -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Ping.java b/app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Ping.java deleted file mode 100644 index b5fc374..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Functionality/Ping.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.lcl.lclmeasurementtool.Functionality; - -/** - * Ping is a functionality module that is able to - * run ping test to test the reachability between a host and the current device. - */ - -public class Ping { - -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/LCLApplication.kt b/app/src/main/java/com/lcl/lclmeasurementtool/LCLApplication.kt new file mode 100644 index 0000000..1bd77bd --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/LCLApplication.kt @@ -0,0 +1,23 @@ +package com.lcl.lclmeasurementtool + +import android.app.Application +import android.util.Log +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import com.lcl.lclmeasurementtool.sync.Sync +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject + +/** + * [Application] class for LCL Measurement tool + */ +@HiltAndroidApp +//class LCLApplication : Application(), Configuration.Provider { +class LCLApplication : Application() { + override fun onCreate() { + super.onCreate() + + // Initialize Sync; the system responsible for keeping data in the app up to date. + Sync.initialize(context = this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity.java b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity.java deleted file mode 100644 index fe4884c..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity.java +++ /dev/null @@ -1,206 +0,0 @@ -package com.lcl.lclmeasurementtool; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.content.Context; -import android.os.Bundle; -import android.provider.Settings; -import android.util.Log; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.lcl.lclmeasurementtool.Managers.CellularManager; -import com.lcl.lclmeasurementtool.Managers.LocationServiceListener; -import com.lcl.lclmeasurementtool.Managers.LocationServiceManager; -import com.lcl.lclmeasurementtool.Managers.NetworkChangeListener; -import com.lcl.lclmeasurementtool.Managers.NetworkManager; -import com.lcl.lclmeasurementtool.Utils.SignalStrengthLevel; -import com.lcl.lclmeasurementtool.Utils.UIUtils; -import java.util.UUID; -import com.lcl.lclmeasurementtool.Utils.UnitUtils; - -public class MainActivity extends AppCompatActivity { - - public static final String TAG = "MAIN_ACTIVITY"; - private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34; - - private Context context; - CellularManager mCellularManager; - NetworkManager mNetworkManager; - LocationServiceManager mLocationManager; - LocationServiceListener locationServiceListener; - - private boolean isTestStarted; - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - SharedPreferences preferences = getPreferences(MODE_PRIVATE); - if (!preferences.contains(getString(R.string.USER_UUID))) { - String uuid = UUID.randomUUID().toString(); - SharedPreferences.Editor editor = preferences.edit(); - editor.putString(getString(R.string.USER_UUID), uuid); - editor.apply(); - } - - mNetworkManager = NetworkManager.getManager(this); - mCellularManager = CellularManager.getManager(this); - mLocationManager = LocationServiceManager.getManager(this.getApplicationContext()); - locationServiceListener = new LocationServiceListener(this, getLifecycle()); - getLifecycle().addObserver(locationServiceListener); - this.context = this; - this.isTestStarted = false; - - if (!this.mNetworkManager.isCellularConnected()) { - updateSignalStrengthTexts(SignalStrengthLevel.NONE, 0); - } - - setUpFAB(); - updateFAB(this.mNetworkManager.isCellularConnected()); - - this.mNetworkManager.addNetworkChangeListener(new NetworkChangeListener() { - @Override - public void onAvailable() { - Log.i(TAG, "from call back on available"); - updateFAB(true); - mCellularManager.listenToSignalStrengthChange((level, dBm) -> - updateSignalStrengthTexts(level, dBm)); - } - - @Override - public void onLost() { - mCellularManager.stopListening(); - Log.e(TAG, "on lost"); - updateSignalStrengthTexts(SignalStrengthLevel.NONE, 0); - updateFAB(false); - } - - @Override - public void onUnavailable() { - Log.e(TAG, "on unavailable"); - updateSignalStrengthTexts(SignalStrengthLevel.NONE, 0); - updateFAB(false); - } - - @Override - public void onCellularNetworkChanged(boolean isConnected) { - Log.e(TAG, "on connection lost"); - if (!isConnected) { - updateSignalStrengthTexts(SignalStrengthLevel.NONE, 0); - updateFAB(false); - } - } - }); - } - - private void updateSignalStrengthTexts(SignalStrengthLevel level, int dBm) { - runOnUiThread(() -> { - TextView signalStrengthValue = findViewById(R.id.SignalStrengthValue); - TextView signalStrengthStatus = findViewById(R.id.SignalStrengthStatus); - TextView signalStrengthUnit = findViewById(R.id.SignalStrengthUnit); - ImageView signalStrengthIndicator = findViewById(R.id.SignalStrengthIndicator); - signalStrengthValue.setText(String.valueOf(dBm)); - signalStrengthUnit.setText(UnitUtils.SIGNAL_STRENGTH_UNIT); - signalStrengthStatus.setText(level.getName()); - signalStrengthIndicator.setColorFilter(level.getColor(context)); - }); - } - - private void setUpFAB() { - FloatingActionButton fab = findViewById(R.id.fab); - fab.setOnClickListener(button -> { - ((FloatingActionButton) button).setImageResource( this.isTestStarted ? R.drawable.start : R.drawable.stop ); - fab.setColorFilter(ContextCompat.getColor(this, R.color.purple_500)); - - // TODO: init/cancel ping and iperf based in iTestStart - - this.isTestStarted = !isTestStarted; - Toast.makeText(this, "test starts: " + this.isTestStarted, Toast.LENGTH_SHORT).show(); - }); - } - - private void updateFAB(boolean state) { - runOnUiThread(() -> { - FloatingActionButton fab = findViewById(R.id.fab); - fab.setEnabled(state); - fab.setImageResource(R.drawable.start); - fab.setColorFilter(state ? ContextCompat.getColor(this, R.color.purple_500) : - ContextCompat.getColor(this, R.color.light_gray)); - -// TODO: cancel ping and iperf if started -// if (isTestStarted) { - // cancel test -// } - - this.isTestStarted = false; - }); - } - - - // TODO: update FAB Icon and State when tests are done - - - @Override - protected void onDestroy() { - super.onDestroy(); - this.mCellularManager.stopListening(); - this.mNetworkManager.removeAllNetworkChangeListeners(); - } - - - ////////////////// HELPER FUNCTION /////////////////////// - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) { - if (grantResults.length <= 0) { - // If user interaction was interrupted, the permission request is cancelled and we - // receive empty arrays. - Log.e(TAG, "User interaction was cancelled."); - } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Permission granted. - Log.i(TAG, "Location permission granted"); - } else { - // Permission denied. - - // Notify the user via a dialog that they have rejected a core permission for the - // app, which makes the Activity useless. - UIUtils.showDialog(this, - R.string.location_message_title, - R.string.permission_denied_explanation, - R.string.settings, - (dialogInterface, actionID) -> { - - // Build intent that displays the App settings screen. - Intent intent = new Intent(); - intent.setAction( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", - BuildConfig.APPLICATION_ID, null); - intent.setData(uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - }, android.R.string.cancel, null); - } - } - } - - /** - * Fetch the last location from the device - */ - private void getLastLocation() { - mLocationManager.getLastLocation(); - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt new file mode 100644 index 0000000..ee68beb --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivity2.kt @@ -0,0 +1,158 @@ +package com.lcl.lclmeasurementtool + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.view.WindowCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.viewbinding.BuildConfig +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import com.kongzue.dialogx.dialogs.MessageDialog +import com.lcl.lclmeasurementtool.networking.NetworkMonitor +import com.lcl.lclmeasurementtool.networking.SimStateMonitor +import com.lcl.lclmeasurementtool.ui.LCLApp +import com.lcl.lclmeasurementtool.ui.Login +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import java.security.* +import java.util.* +import javax.inject.Inject + +@AndroidEntryPoint +class MainActivity2 : ComponentActivity() { + + companion object { + const val TAG = "MainActivity" + } + + @Inject + lateinit var networkMonitor: NetworkMonitor + + @Inject + lateinit var simStateMonitor: SimStateMonitor + + private val viewModel: MainActivityViewModel by viewModels() + + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + + if (!hasPermission()) { + XXPermissions.with(this) + .permission(Permission.CAMERA, Permission.READ_EXTERNAL_STORAGE, Permission.ACCESS_FINE_LOCATION) + .request { _, allGranted -> + run { + if (!allGranted) { + + // Permission denied. + + // Notify the user via a dialog that they have rejected a core permission for the + // app, which makes the Activity useless. + MessageDialog.build() + .setTitle(R.string.location_message_title) + .setMessage(R.string.permission_denied_explanation) + .setOkButton(R.string.settings) { baseDialog, v -> + // Build intent that displays the App settings screen. + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + val uri = Uri.fromParts( + "package", + BuildConfig.LIBRARY_PACKAGE_NAME, null + ) + intent.data = uri + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + false + }.setOkButton(android.R.string.cancel).show() + } + } + } + } + + var uiState: MainActivityUiState by mutableStateOf(MainActivityUiState.Login) + + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState + .onEach { + uiState = it + } + .collect() + } + } + + Log.d(TAG, "uiState is ${uiState is MainActivityUiState.Login}") + + // Turn off the decor fitting system windows, which allows us to handle insets, + // including IME animations + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + if (com.lcl.lclmeasurementtool.BuildConfig.FLAVOR != "dev") { + if (uiState == MainActivityUiState.Login) { + viewModel.setDeviceId(UUID.randomUUID().toString()) + Login(viewModel = viewModel) + return@setContent + } + } + LCLApp(windowSizeClass = calculateWindowSizeClass(activity = this), networkMonitor, simStateMonitor) + } + } + + private fun hasPermission(): Boolean { + return XXPermissions.isGranted( + this, + Permission.CAMERA, + Permission.READ_EXTERNAL_STORAGE, + Permission.ACCESS_FINE_LOCATION + ) + } +} + +sealed interface ScanStatus { + data class ScanSuccess(val sigmaTHex: ByteArray, val pkAHex: ByteArray, val skTHex: ByteArray): ScanStatus { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ScanSuccess + + if (!sigmaTHex.contentEquals(other.sigmaTHex)) return false + if (!pkAHex.contentEquals(other.pkAHex)) return false + if (!skTHex.contentEquals(other.skTHex)) return false + + return true + } + + override fun hashCode(): Int { + var result = sigmaTHex.contentHashCode() + result = 31 * result + pkAHex.contentHashCode() + result = 31 * result + skTHex.contentHashCode() + return result + } + } + + object KeyVerificationFailed: ScanStatus + data class KeyVerificationException(val exception: Exception) : ScanStatus +} + +open class LoginStatus { + object Initial: LoginStatus() + data class RegistrationFailed(val reason: String): LoginStatus() + object RegistrationSucceeded: LoginStatus() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt new file mode 100644 index 0000000..05fae3c --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/MainActivityViewModel.kt @@ -0,0 +1,470 @@ +package com.lcl.lclmeasurementtool + +import android.os.Build +import android.telephony.CellSignalStrength +import android.util.Log +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.protobuf.ByteString +import com.lcl.lclmeasurementtool.constants.NetworkConstants +import com.lcl.lclmeasurementtool.features.mlab.MLabRunner +import com.lcl.lclmeasurementtool.features.mlab.MLabTestStatus +import com.lcl.lclmeasurementtool.features.ping.Ping +import com.lcl.lclmeasurementtool.features.ping.PingError +import com.lcl.lclmeasurementtool.features.ping.PingErrorCase +import com.lcl.lclmeasurementtool.features.ping.PingResult +import com.lcl.lclmeasurementtool.location.LocationService +import com.lcl.lclmeasurementtool.model.datamodel.* +import com.lcl.lclmeasurementtool.model.repository.ConnectivityRepository +import com.lcl.lclmeasurementtool.model.repository.NetworkApiRepository +import com.lcl.lclmeasurementtool.model.repository.SignalStrengthRepository +import com.lcl.lclmeasurementtool.model.repository.UserDataRepository +import com.lcl.lclmeasurementtool.telephony.SignalStrengthLevelEnum +import com.lcl.lclmeasurementtool.telephony.SignalStrengthMonitor +import com.lcl.lclmeasurementtool.util.* +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import net.measurementlab.ndt7.android.NDTTest +import okhttp3.ResponseBody +import retrofit2.HttpException +import java.io.ByteArrayOutputStream +import java.security.SecureRandom +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import javax.inject.Inject + +@HiltViewModel +class MainActivityViewModel @Inject constructor( + private val userDataRepository: UserDataRepository, + private val networkApi: NetworkApiRepository, + private val locationService: LocationService, + private val signalStrengthMonitor: SignalStrengthMonitor, + private val connectivityRepository: ConnectivityRepository, + private val signalStrengthRepository: SignalStrengthRepository +) : ViewModel() { + + companion object { + const val TAG = "MainActivityViewModel" + } + + // UI + var uiState: StateFlow = userDataRepository.userData.map { + if (it.loggedIn) MainActivityUiState.Success(it) else MainActivityUiState.Login + }.stateIn( + scope = viewModelScope, + initialValue = MainActivityUiState.Login, + started = SharingStarted.WhileSubscribed(5_000) + ) + private set + + private var _loginState = MutableStateFlow(LoginStatus()) + var loginState = _loginState.asStateFlow() + + // Network Testing + private val _isMLabTestActive = MutableStateFlow(false) + + private var _mLabPingResult = MutableStateFlow(PingResultState()) + private var _mLabUploadResult = MutableStateFlow(ConnectivityTestResult()) + private var _mLabDownloadResult = MutableStateFlow(ConnectivityTestResult()) + + var mLabPingResult = _mLabPingResult.asStateFlow() + var mlabUploadResult = _mLabUploadResult.asStateFlow() + var mlabDownloadResult = _mLabDownloadResult.asStateFlow() + val isMLabTestActive = _isMLabTestActive.asStateFlow() + private var mlabTestJob: Job? = null + + // Authentication + fun login(hPKR: ByteString, skT: ByteString) = viewModelScope.launch { + userDataRepository.setKeys(hPKR, skT) + } + + fun logout() = viewModelScope.launch { + _loginState.value = LoginStatus.Initial + userDataRepository.logout() + } + + fun setR(R: ByteString) = viewModelScope.launch { + userDataRepository.setR(R) + } + + fun setDeviceId(id: String) = viewModelScope.launch { + userDataRepository.setDeviceID(id) + } + + fun demoLogin() { + login(hPKR = ByteString.EMPTY, skT = ByteString.EMPTY) + _loginState.value = LoginStatus.RegistrationSucceeded + } + + suspend fun login(result: String) { + val job = viewModelScope.async { + val jsonObj: QRCodeKeysModel + try { + jsonObj = Json.decodeFromString(result) + } catch (e: SerializationException) { + Log.d(TAG, "The QR Code is invalid. Please rescan the code or contact the administrator at lcl@seattlecommunitynetwork.org.") +// val reasons = +// AnalyticsUtils.formatProperties(e.message, Arrays.toString(e.stackTrace)) +// Analytics.trackEvent(AnalyticsUtils.QR_CODE_PARSING_FAILED, reasons) + _loginState.value = LoginStatus.RegistrationFailed("QRCodeParseFailed") + return@async + } + + Log.d(TAG, "the scanner result is $result") + + val sigma_t = jsonObj.sigmaT + val sk_t = jsonObj.skT + val pk_a = jsonObj.pk_a + + when (val validationResult = validate(sigmaT = sigma_t, pkA = pk_a, skT = sk_t)) { + is ScanStatus.KeyVerificationFailed -> { + Log.d(TAG, "KeyVerificationFailed") + _loginState.value = LoginStatus.RegistrationFailed("KeyVerificationFailed") + return@async + } + + is ScanStatus.KeyVerificationException -> { + Log.d(TAG, "KeyVerificationException") + _loginState.value = LoginStatus.RegistrationFailed(validationResult.exception.toString()) + return@async + } + + is ScanStatus.ScanSuccess -> { + val skTHex = validationResult.skTHex + val pk_t: ECPublicKey + val ecPrivateKey: ECPrivateKey + try { + ecPrivateKey = ECDSA.DeserializePrivateKey(skTHex) + pk_t = ECDSA.DerivePublicKey(ecPrivateKey) + } catch (e: Exception) { + Log.d(TAG, "KeyGenerationFailed") + _loginState.value = LoginStatus.RegistrationFailed("KeyGenerationFailed") + return@async + } + + val secureRandom = SecureRandom() + val r = ByteArray(16) + secureRandom.nextBytes(r) + setR(ByteString.copyFrom(r)) + // TODO: maybe zero out the R array + val byteArray = ByteArrayOutputStream() + val h_pkr: ByteArray + val h_sec: ByteArray + val h_concat: ByteArray + val sigma_r: ByteArray + try { + withContext(Dispatchers.IO) { + byteArray.write(pk_t.encoded) + byteArray.write(r) + h_pkr = SecurityUtil.digest(byteArray.toByteArray(), SecurityUtil.SHA_256_HASH) + byteArray.reset() + byteArray.write(skTHex) + byteArray.write(pk_t.encoded) + h_sec = SecurityUtil.digest(byteArray.toByteArray(), SecurityUtil.SHA_256_HASH) + byteArray.reset() + byteArray.write(h_pkr) + byteArray.write(h_sec) + h_concat = byteArray.toByteArray() + } + sigma_r = ECDSA.Sign(h_concat, ecPrivateKey) + } catch (e: Exception) { + Log.d(TAG, "KeySignFailed") + _loginState.value = LoginStatus.RegistrationFailed("KeySignFailed") + return@async + } + + val registration = Json.encodeToString( + RegistrationModel( + Hex.encodeHexString(sigma_r, false), + Hex.encodeHexString(h_concat, false), + Hex.encodeHexString(r, false) + ) + ) + + try { + networkApi.register(registration) + login(ByteString.copyFrom(h_pkr), ByteString.copyFrom(Hex.decodeHex(sk_t))) + _loginState.value = LoginStatus.RegistrationSucceeded + } catch (e: HttpException) { + Log.d(TAG, "error occurred during registration. error is $e") + _loginState.value = LoginStatus.RegistrationFailed(e.message()) + } + + return@async + } + else -> { + Log.d(TAG, "UnexpectedErrorOccurred") + _loginState.value = LoginStatus.RegistrationFailed("UnexpectedErrorOccurred") + return@async + } + } + } + return job.await() + } + + private fun validate(sigmaT: String, pkA: String, skT: String): ScanStatus { + val sigmaTHex: ByteArray + val pkAHex: ByteArray + val skTHex: ByteArray + try { + sigmaTHex = Hex.decodeHex(sigmaT) + pkAHex = Hex.decodeHex(pkA) + skTHex = Hex.decodeHex(skT) + if (!ECDSA.Verify( + skTHex, + sigmaTHex, + ECDSA.DeserializePublicKey(pkAHex)) + ) { + return ScanStatus.KeyVerificationFailed + } + } catch (e: Exception) { + return ScanStatus.KeyVerificationException(e) + } + + return ScanStatus.ScanSuccess(sigmaTHex, pkAHex, skTHex) + } + + var signalStrengthResult = signalStrengthMonitor.signalStrength.map {s -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val report = s.getCellSignalStrengths(CellSignalStrength::class.java) + if (report.isEmpty()) { + SignalStrengthResult(0, SignalStrengthLevelEnum.POOR) + } else { + val data = report[0] + SignalStrengthResult(data.dbm, SignalStrengthLevelEnum.init(data.level)) + } + } else { + val dBm = s.getGsmSignalStrength() + val level = SignalStrengthLevelEnum.init(s.level) + SignalStrengthResult(dBm, level) + } + }.stateIn( + scope = viewModelScope, + initialValue = SignalStrengthResult(0, SignalStrengthLevelEnum.POOR), + started = SharingStarted.WhileSubscribed(5_000) + ) + + private suspend fun runMLabPing() { + try { + Ping.cancellableStart(address = NetworkConstants.PING_TEST_ADDRESS, timeout = 1000) + .onStart { + Log.d(TAG, "isActive = true") + _isMLabTestActive.value = true + } + .onCompletion { + if (it != null) { + Log.d(TAG, "Error is ${it.message}") + _isMLabTestActive.value = false + } + } + .collect { + _mLabPingResult.value = when(it.error.code) { + PingErrorCase.OK -> PingResultState.Success(it) + else -> { + _isMLabTestActive.value = false + PingResultState.Error(it.error) + } + } + } + } catch (e: IllegalArgumentException) { + _mLabPingResult.value = PingResultState.Error(PingError(PingErrorCase.OTHER, e.message)) + Log.e(TAG, "Ping Config error") + } + } + + fun cancelMLabTest() { + Log.d(TAG, "cancellation: the test job is $mlabTestJob") + mlabTestJob?.cancel(CancellationException("Shit, cancel this test!!!")) + Log.d(TAG, "Tests cancelled") + resetMLabTestResult() + } + + private suspend fun getMLabTestResult() { + try { + MLabRunner.runTest(NDTTest.TestType.DOWNLOAD_AND_UPLOAD) + .onStart { + _isMLabTestActive.value = true + } + .onCompletion { + if (it != null) { + Log.d(TAG, "Error is ${it.message}") + _isMLabTestActive.value = false + } + } + .collect{ + when(it.type) { + NDTTest.TestType.UPLOAD -> { + _mLabUploadResult.value = when(it.status) { + MLabTestStatus.RUNNING -> { ConnectivityTestResult.Result(it.speed!!, Color.LightGray) } + + MLabTestStatus.FINISHED -> { ConnectivityTestResult.Result(it.speed!!, Color.Black) } + + MLabTestStatus.ERROR -> { + _isMLabTestActive.value = false + ConnectivityTestResult.Error(it.errorMsg!!) + } + } + } + NDTTest.TestType.DOWNLOAD -> { + _mLabDownloadResult.value = when(it.status) { + MLabTestStatus.RUNNING -> { ConnectivityTestResult.Result(it.speed!!, Color.LightGray) } + + MLabTestStatus.FINISHED -> { ConnectivityTestResult.Result(it.speed!!, Color.Black) } + + MLabTestStatus.ERROR -> { + _isMLabTestActive.value = false + ConnectivityTestResult.Error(it.errorMsg!!) + } + } + } + else -> { } + } + } + } catch (e: Exception) { + Log.d(TAG, "catch $e") + } + } + + fun runMLabTest() { + + if (mlabTestJob?.isActive == true) { + mlabTestJob?.cancel() + } + + mlabTestJob = viewModelScope.launch { + try { + resetMLabTestResult() + + runMLabPing() + if (_mLabPingResult.value is PingResultState.Error) { + this.cancel("Ping Test Failed") + } + ensureActive() + + getMLabTestResult() + if (_mLabUploadResult.value is ConnectivityTestResult.Error || _mLabDownloadResult.value is ConnectivityTestResult.Error) { + Log.d(TAG, "mlab test job is cancelled") + this.cancel("MLab test failed") + } + if (isActive) { + Log.d(TAG, "mlab test job is still active") + } else { + Log.d(TAG, "mlab test job is completed") + } + + ensureActive() + + _isMLabTestActive.value = false + Log.d(TAG, "ping, upload, download are finished. isMLabTestActive.value=${isMLabTestActive.value}") + val curTime = TimeUtil.getCurrentTime() + val cellID = signalStrengthMonitor.getCellID() + + // add data to db + report to remote server + locationService.lastLocation().combine(userDataRepository.userData) { location, userPreference -> + Pair(location, userPreference) + }.collect { + val signalStrengthReportModel = SignalStrengthReportModel( + it.first.latitude, + it.first.longitude, + curTime, + cellID, + it.second.deviceID, + signalStrengthResult.value.dbm, + signalStrengthResult.value.level.level + ) + + val connectivityReportModel = ConnectivityReportModel( + it.first.latitude, + it.first.longitude, + curTime, + cellID, + it.second.deviceID, + (_mLabUploadResult.value as ConnectivityTestResult.Result).result.toDouble(), + (_mLabDownloadResult.value as ConnectivityTestResult.Result).result.toDouble(), + (_mLabPingResult.value as PingResultState.Success).result.avg!!.toDouble(), + (_mLabPingResult.value as PingResultState.Success).result.numLoss!!.toDouble(), + ) + + saveToDB(signalStrengthReportModel, connectivityReportModel) + + report(signalStrengthReportModel, it.second) + report(connectivityReportModel, it.second) + } + } catch (e: Exception) { + Log.d(TAG, "catch $e") + } + } + } + + private suspend fun saveToDB(signalStrengthReportModel: SignalStrengthReportModel, connectivityReportModel: ConnectivityReportModel) { + signalStrengthRepository.insert(signalStrengthReportModel) + connectivityRepository.insert(connectivityReportModel) + } + + private suspend fun report(reportModel: BaseMeasureDataModel, userData: UserData) { + if (BuildConfig.FLAVOR != "full") { + Log.d(TAG, "Only with ProductFlavor *full* will the data be reported to the remote server") + return + } + + try { + val reportString = prepareReportData(reportModel, userData) + val response: ResponseBody = if (reportModel is SignalStrengthReportModel) { + networkApi.uploadSignalStrength(reportString) + } else { + networkApi.uploadConnectivity(reportString) + } + + Log.i(TAG, "report success! response is $response") + + // update DB to reflect the change + if (reportModel is SignalStrengthReportModel) { + signalStrengthRepository.update(reportModel.copy(reported = true)) + } else if (reportModel is ConnectivityReportModel) { + connectivityRepository.update(reportModel.copy(reported = true)) + } + return + + } catch (e: HttpException) { + // TODO: retry + if (e.code() in 400..499) { + Log.d(TAG, "client error") + } + + if (e.code() in 500..599) { + Log.d(TAG, "server error") + } + } catch (e: Exception) { + Log.d(TAG, "unknown exception occurred when uploading data. $e") + } + } + + private fun resetMLabTestResult() { + _mLabPingResult.value = PingResultState.Error(PingError(PingErrorCase.OK, null)) + _mLabUploadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) + _mLabDownloadResult.value = ConnectivityTestResult.Result("0.0", Color.LightGray) + } +} + +sealed interface MainActivityUiState { + object Login : MainActivityUiState + data class Success(val userData: UserData) : MainActivityUiState +} + +open class ConnectivityTestResult { + data class Result (val result: String, val color: Color): ConnectivityTestResult() + data class Error(val error: String): ConnectivityTestResult() +} + +open class PingResultState { + data class Success(val result: PingResult): PingResultState() + data class Error(val error: PingError): PingResultState() +} + +data class SignalStrengthResult(val dbm: Int, val level: SignalStrengthLevelEnum) \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularChangeListener.java b/app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularChangeListener.java deleted file mode 100644 index da36c24..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularChangeListener.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.lcl.lclmeasurementtool.Managers; - -import com.lcl.lclmeasurementtool.Utils.SignalStrengthLevel; - -/** - * Listen to the cellular network changes from the device. - */ -public interface CellularChangeListener { - - /** - * Callback function when the cellular network changes. - * @param level the Signal Strength level. - * @param dBm the signal strength in dBm. - */ - void onChange(SignalStrengthLevel level, int dBm); -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularManager.java b/app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularManager.java deleted file mode 100644 index ea929af..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/CellularManager.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.lcl.lclmeasurementtool.Managers; -import android.content.Context; -import android.os.Looper; -import android.telephony.CellSignalStrength; -import android.telephony.CellSignalStrengthGsm; -import android.telephony.CellSignalStrengthLte; -import android.telephony.PhoneStateListener; -import android.telephony.SignalStrength; -import android.telephony.TelephonyManager; -import android.util.Log; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import com.lcl.lclmeasurementtool.Utils.SignalStrengthLevel; - -import java.util.List; - -/** - * CellularManager monitors changes in device's signal strength and - * report changes(callback) to front-end UI - * @see CellSignalStrengthLte - */ -public class CellularManager { - - // LOG TAG constant - static final String TAG = "CELLULAR_MANAGER_TAG"; - - private static CellularManager cellularManager = null; - - // the telephony manager that manages all access related to cellular information. - private final TelephonyManager telephonyManager; - - // the signal strength object that stores the information - // retrieved from the system by the time the object is accessed. - private final SignalStrength signalStrength; - - // the LTE signal strength report that consists of all cellular signal strength information. - private final CellSignalStrength report; - - // the flag that controls when to stop listening to signal strength change. - private boolean stopListening; - - /** - * Construct a new CellularManager object based on current context. - * @param context the context of the application. - */ - private CellularManager(@NonNull Context context) { - this.telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - this.signalStrength = this.telephonyManager.getSignalStrength(); - if (this.signalStrength.getCellSignalStrengths().size() > 0) { - this.report = this.signalStrength.getCellSignalStrengths().get(0); - } else { - this.report = null; - } - } - - /** - * Retrieve the cellular manager object from current context. - * @return a cellular manager - */ - public static CellularManager getManager(@NonNull Context context) { - if (cellularManager == null) { - cellularManager = new CellularManager(context); - } - return cellularManager; - } - - /** - * Retrieve the signal strength object from current context. - * @return a signal strength object. - */ - public SignalStrength getSignalStrength() { - return this.signalStrength; - } - - /** - * Retrieve the signalStrengthLevel Enum from current context. - * @return a corresponding signal strength level from the current context. - */ - public SignalStrengthLevel getSignalStrengthLevel() { - if (this.report != null) { - int level = this.report.getLevel(); - return SignalStrengthLevel.init(level); - } - - return SignalStrengthLevel.NONE; - } - - /** - * Retrieve the CellSignalStrength report. - * @return a CellSignalStrength object that - * contains all information related to cellular signal strength. - * report might be null if no cellular connection. - */ - public CellSignalStrength getCellSignalStrength() { - return this.report; - } - - /** - * Retrieve the signal Strength in dBm. - * @return an integer of signal strength in dBm. - * If no cellular connection, 0. - */ - public int getDBM() { - return this.report != null ? this.report.getDbm() : 0; - } - - /** - * Start listen to signal strength change and display onto the corresponding TextView. - * - */ - public void listenToSignalStrengthChange(CellularChangeListener listener) { - new Thread(new Runnable() { - @Override - public void run() { - stopListening = false; - Looper.prepare(); - - telephonyManager.listen(new PhoneStateListener() { - @Override - public void onSignalStrengthsChanged(SignalStrength signalStrength) { - super.onSignalStrengthsChanged(signalStrength); - List reports = signalStrength.getCellSignalStrengths(); - - - int dBm; - SignalStrengthLevel level; - if (reports.size() > 0) { - CellSignalStrength report = reports.get(0); - level = SignalStrengthLevel.init(report.getLevel()); - dBm = report.getDbm(); - } else { - level = SignalStrengthLevel.NONE; - dBm = level.getLevelCode(); - } - - listener.onChange(level, dBm); - - if (stopListening) { - Looper.myLooper().quit(); - } - } - }, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS); - - Looper.loop(); - } - }).start(); - } - - /** - * Stop listening the changes on signal strength. - */ - public void stopListening() { - this.stopListening = true; - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceListener.java b/app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceListener.java deleted file mode 100644 index eb6832f..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceListener.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.lcl.lclmeasurementtool.Managers; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.provider.Settings; - -import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; - -import com.lcl.lclmeasurementtool.MainActivity; -import com.lcl.lclmeasurementtool.R; -import com.lcl.lclmeasurementtool.Utils.UIUtils; - -public class LocationServiceListener implements LifecycleObserver { - - private static final String TAG = "LOCATION_SERVICE_LISTENER"; - - // permission code - private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34; - - // the Location service object - private final LocationServiceManager mLocationManager; - - // the context of the Application - private final Context context; - - // the life cycle object - private final Lifecycle lifecycle; - - // the lock that controls the check on location mode - private boolean checkLocationModeLock = false; - - /** - * Initialize a LocationService Listener using the current context of the Application - * - * @param context the context of the application - */ - public LocationServiceListener(@NonNull Context context, Lifecycle lifecycle) { - this.context = context; - this.mLocationManager = LocationServiceManager.getManager(context); - this.lifecycle = lifecycle; - } - - - /** - * Check the location permission during on_resume in app's lifecycle - */ - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - void checkLocationPermission() { - if (!mLocationManager.isLocationPermissionGranted()) { - requestLocationPermission(); - } - } - - /** - * Check the location service during on_resume in app's lifecycle - */ - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - void checkLocationMode() { - if (!mLocationManager.isLocationModeOn() && lifecycle.getCurrentState().equals(Lifecycle.State.RESUMED) && !checkLocationModeLock) { - checkLocationModeLock = true; - // TODO turn off start FAB if canceled - UIUtils.showDialog(context, R.string.location_message_title, R.string.enable_location_message, - R.string.go_to_setting, - (paramDialogInterface, paramInt) -> { - context.startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)); - checkLocationModeLock = false; - }, - android.R.string.cancel, - null); - } - } - - /** - * Request location permission to users - */ - private void requestLocationPermission() { - boolean shouldProvideRationale = - ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, - Manifest.permission.ACCESS_FINE_LOCATION); - - if (shouldProvideRationale) { - UIUtils.showDialog(context, R.string.location_message_title, R.string.permission_rationale, - android.R.string.ok, - (a, b) -> startLocationPermissionRequest(), - android.R.string.cancel, null); - } else { - startLocationPermissionRequest(); - } - } - - /** - * Start the permission request - */ - private void startLocationPermissionRequest() { - ActivityCompat.requestPermissions((Activity) context, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - REQUEST_PERMISSIONS_REQUEST_CODE); - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceManager.java b/app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceManager.java deleted file mode 100644 index 028814d..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/LocationServiceManager.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.lcl.lclmeasurementtool.Managers; - -import android.Manifest; -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.Location; -import android.location.LocationManager; -import android.util.Log; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; - -import com.google.android.gms.location.FusedLocationProviderClient; -import com.google.android.gms.location.LocationServices; -import com.lcl.lclmeasurementtool.R; - -import java.lang.ref.WeakReference; - -public class LocationServiceManager { - - private static final String TAG = "LOCATION_MANAGER"; - - // the location service manager instance - private static LocationServiceManager locationServiceManager = null; - - // the fused location client provided by Google Play Service - private final FusedLocationProviderClient mFusedLocationClient; - - // the instance of the location manager - private final LocationManager locationManager; - - // the weak reference of the current context of the Application - private final WeakReference context; - - // the location object that contains the information of user's location - private Location mLastLocation; - - /** - * Initialize a Location Service Manager following the context - * - * @param context the context of the current activity - */ - private LocationServiceManager(@NonNull Context context) { - this.context = new WeakReference<>(context); - locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this.context.get()); - } - - /** - * Return a LocationService Manager instance following the given context. - * - * @param context the context of the current activity - * @return a location service manager instance - */ - public static LocationServiceManager getManager(@NonNull Context context) { - if (locationServiceManager == null) { - locationServiceManager = new LocationServiceManager(context); - } - return locationServiceManager; - } - - /** - * Return whether the device's location mode is on. - * @return whether the device's location mode is on. - */ - public boolean isLocationModeOn() { - return locationManager.isLocationEnabled(); - } - - /** - * Return whether the device's location permission is granted. - * - * @return whether the device's location permission is granted - */ - public boolean isLocationPermissionGranted() { - int permissionState = ActivityCompat.checkSelfPermission(context.get(), - Manifest.permission.ACCESS_FINE_LOCATION); - return permissionState == PackageManager.PERMISSION_GRANTED; - } - - /** - * Retrieve the last location from the device. - * If last location is null, a new location request will be initiated. - */ - @SuppressWarnings("MissingPermission") - public void getLastLocation() { - mFusedLocationClient.getLastLocation() - .addOnCompleteListener((Activity) context.get(), task -> { - if (task.isSuccessful() && task.getResult() != null) { - mLastLocation = task.getResult(); - - // TODO: log the location latitude and longitude - Log.i(TAG, String.valueOf(mLastLocation.getLatitude())); - Log.i(TAG, String.valueOf(mLastLocation.getLongitude())); - } else { - Log.w(TAG, "getLastLocation:exception", task.getException()); - Toast.makeText(context.get(), context.get().getText(R.string.no_location_detected), Toast.LENGTH_SHORT).show(); - } - }); - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkChangeListener.java b/app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkChangeListener.java deleted file mode 100644 index d05311d..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkChangeListener.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.lcl.lclmeasurementtool.Managers; - -/** - * An interface that listens to the changes in the network status. - */ -public interface NetworkChangeListener { - - /** - * callback function when the cellular network becomes available. - */ - void onAvailable(); - - /** - * callback function when the cellular network becomes unavailable. - */ - void onUnavailable(); - - /** - * callback function when the cellular network gets lost. - */ - void onLost(); - - /** - * callback function when the cellular network status gets changed. - * - * @param isConnected boolean parameter indicating whether there is cellular connection. - */ - void onCellularNetworkChanged(boolean isConnected); -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkManager.java b/app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkManager.java deleted file mode 100644 index 92fdfbb..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Managers/NetworkManager.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.lcl.lclmeasurementtool.Managers; - -import android.net.ConnectivityManager; -import android.content.Context; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkRequest; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; - -import androidx.annotation.NonNull; - - -/** - * NetworkManager manages all network related information, including but not limited to - * connectivity states, active network information(wifi, cellular) and - * listen to changes in network states. - * - * @see reading network state - */ -public class NetworkManager { - - // LOG TAG constant - private static final String TAG = "NETWORK_MANAGER_TAG"; - - private static NetworkManager networkManager = null; - - // A List of registered ColorChangeListeners - private final List mNetworkChangeListeners; - - // A Network Callback object - private ConnectivityManager.NetworkCallback networkCallback; - - // the connectivity manager object that keeps track of all information - // related to phone's connectivity states. - private final ConnectivityManager connectivityManager; - - // current device supports with regards to networking. - private final NetworkCapabilities capabilities; - - /** - * Initialize a Network Manager object following the context of current device. - * @param context the Context object of the current device - */ - private NetworkManager(@NonNull Context context) { - this.connectivityManager = - (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - - // the network object that encapsulates all info regarding Network - Network network = this.connectivityManager.getActiveNetwork(); - this.capabilities = this.connectivityManager.getNetworkCapabilities(network); - this.mNetworkChangeListeners = new ArrayList<>(); - } - - /** - * Retrieve the network manager object from current context. - * @return a network manager - */ - public static NetworkManager getManager(@NonNull Context context) { - if (networkManager == null) { - networkManager = new NetworkManager(context); - } - - return networkManager; - } - - /** - * Registers a new listener - * - * @param networkChangeListener a new listener (should not be null). - */ - public void addNetworkChangeListener(@NonNull NetworkChangeListener networkChangeListener) { - this.mNetworkChangeListeners.add(networkChangeListener); - - NetworkRequest request = new NetworkRequest - .Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) - .build(); - - this.networkCallback = new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(@NonNull Network network) { - super.onAvailable(network); - Log.i(TAG, "current network is " + network); - mNetworkChangeListeners.forEach(NetworkChangeListener::onAvailable); - } - - @Override - public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { - super.onCapabilitiesChanged(network, networkCapabilities); - Log.e(TAG, "The default network changed capabilities: " + networkCapabilities); - mNetworkChangeListeners.forEach(l -> l.onCellularNetworkChanged( - networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) - )); - } - - @Override - public void onLost(@NonNull Network network) { - super.onLost(network); - Log.e(TAG, "The default network lost. Previous one is " + network); - mNetworkChangeListeners.forEach(NetworkChangeListener::onLost); - } - - @Override - public void onUnavailable() { - super.onUnavailable(); - Log.e(TAG, "The default network is unavailable"); - mNetworkChangeListeners.forEach(NetworkChangeListener::onUnavailable); - } - }; - - this.connectivityManager.registerNetworkCallback(request, this.networkCallback); - } - - /** - * Remove a specified listener - * - * @param networkChangeListener the listener to be removed (should not be null) - */ - public void removeNetworkChangeListener(@NonNull NetworkChangeListener networkChangeListener) { - this.mNetworkChangeListeners.remove(networkChangeListener); - } - - /** - * Remove all registered listeners - */ - public void removeAllNetworkChangeListeners() { - this.mNetworkChangeListeners.clear(); - this.connectivityManager.unregisterNetworkCallback(this.networkCallback); - } - - /** - * Returns the current cellular connectivity state of the current device when this method gets called. - * @return true if the current device is connected to the internet via cellular; false otherwise. - */ - public boolean isCellularConnected() { - return this.capabilities != null && - this.capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); - } - - /** - * Retrieve the downstream bandwidth in Kbps - * - * @return the downstream bandwidth in Kbps as an integer. If no cellular capability, return 0. - */ - public int getLinkDownstreamBandwidthKbps() { - if (this.capabilities != null) { - return this.capabilities.getLinkDownstreamBandwidthKbps(); - } - - return 0; - } - - /** - * Retrieve the upstream bandwidth in Kbps - * - * @return the upstream bandwidth in Kbps as an integer. If no cellular capability, return 0. - */ - public int getLinkUpstreamBandwidthKbps() { - if (this.capabilities != null) { - return this.capabilities.getLinkUpstreamBandwidthKbps(); - } - - return 0; - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/AbstractDataTransferRate.java b/app/src/main/java/com/lcl/lclmeasurementtool/Utils/AbstractDataTransferRate.java deleted file mode 100644 index 750af5b..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/AbstractDataTransferRate.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.lcl.lclmeasurementtool.Utils; - -/** - * The abstract interface of a data transfer rate - */ -public interface AbstractDataTransferRate { - /** - * Retrieve the level code of current enum value. - * @return an numeric representation of the enumeration value. - */ - int getLevel(); - - /** - * Retrieve the String representation of the unit. - * @return a string representation of the unit. - * Null value will be returned if the enum mapping fails. - */ - String getUnitString(); -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/ConvertUtils.java b/app/src/main/java/com/lcl/lclmeasurementtool/Utils/ConvertUtils.java deleted file mode 100644 index 30f5c7e..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/ConvertUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.lcl.lclmeasurementtool.Utils; - -/** - * Utilities that will be used to convert units. - */ -public class ConvertUtils { - - /** - * Convert data from one unit to the other. - * - * @param from the base unit to be converted from. - * @param to the destination unit to be converted to. - * @param data the data whose unit will be converted. - * @throws IllegalArgumentException if input data is less than 0. - * @see DataTransferRateUnit - * @return a double in the the destination unit. - */ - public static double convert(DataTransferRateUnit from, - DataTransferRateUnit to, - double data) { - if (data < 0) { - throw new IllegalArgumentException("the input parameter Mbps should be greater than 0"); - } - - double unitConversionRate = 1.0; - if (!from.getUnit().equals(to.getUnit())) { - int diff = from.getUnit().getLevel() - to.getUnit().getLevel(); - unitConversionRate = Math.pow(DataTransferRateUnit.Unit.BASE_CONVERSION_RATE, diff); - } - - double magnitudeConversionRate = 1.0; - if (!from.getMagnitude().equals(to.getMagnitude())) { - int diff = from.getMagnitude().getLevel() - to.getMagnitude().getLevel(); - magnitudeConversionRate = Math.pow(DataTransferRateUnit.Magnitude.BASE_CONVERSION_RATE, - diff); - } - - return data * unitConversionRate * magnitudeConversionRate; - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/DataTransferRateUnit.java b/app/src/main/java/com/lcl/lclmeasurementtool/Utils/DataTransferRateUnit.java deleted file mode 100644 index 5f4c456..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/DataTransferRateUnit.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.lcl.lclmeasurementtool.Utils; - -/** - * An enumeration that maps data transfer rate unit, i.e. kbps, Mb/s - * based on its unit and magnitude. - */ -public enum DataTransferRateUnit { - Kilobit(Unit.Bit, Magnitude.Kilo), - Kilobyte(Unit.Byte, Magnitude.Kilo), - Megabit(Unit.Bit, Magnitude.Mega), - Megabyte(Unit.Byte, Magnitude.Mega), - Gigabit(Unit.Bit, Magnitude.Giga), - Gigabyte(Unit.Byte, Magnitude.Giga); - - // the unit of the rate. - private final Unit unit; - - // the magnitude of the rate. - private final Magnitude magnitude; - - /** - * Initialize a DataTransferRateUnit following the unit and magnitude of the rate. - * @param unit the unit of the data transfer rate. - * @param magnitude the magnitude of the data transfer rate. - */ - DataTransferRateUnit(Unit unit, Magnitude magnitude) { - this.unit = unit; - this.magnitude = magnitude; - } - - /** - * Return the Unit of the enum value. - * @return the corresponding unit of the enum value. - */ - public Unit getUnit() { - return this.unit; - } - - /** - * Retrieve the String representation of the data transfer rate unit. - * @return a string representation of the data transfer rate. - */ - public String getUnitString() { - return this.magnitude.getUnitString() + this.unit.getUnitString() + "/s"; - } - - /** - * Return the Magnitude of the enum value. - * @return the corresponding magnitude of the enum value. - */ - public Magnitude getMagnitude() { - return this.magnitude; - } - - /** - * An enumeration that represents the magnitude of a unit. - */ - public enum Magnitude implements AbstractDataTransferRate { - Kilo(1), // Kilo - Mega(2), // Mega - Giga(3); // Giga - - /** - * the numeric level code for each magnitude. 1 = kilo, 2 = mega, 3 = giga - */ - private final int level; - - /** - * The base conversion rate between any two adjacent magnitudes. - */ - public static final int BASE_CONVERSION_RATE = 1000; - - /** - * Construct a Magnitude enum object. - * @param level the level corresponding to the magnitude object - */ - Magnitude(int level) { - this.level = level; - } - - @Override - public int getLevel() { - return this.level; - } - - @Override - public String getUnitString() { - switch (this) { - case Kilo: - return "K"; - case Mega: - return "M"; - case Giga: - return "G"; - } - return null; - } - } - - /** - * An enumeration that represents the counting unit of a data transfer rate. - */ - public enum Unit implements AbstractDataTransferRate { - Bit(1), // Bit - Byte(2); // Byte - - /** - * the numeric level code for each unit. 1 = bit, 2 = byte - */ - private final int level; - - /** - * The base conversion rate between any two adjacent magnitudes. - */ - public static final int BASE_CONVERSION_RATE = 8; - - /** - * Construct a Unit enum object. - * @param level the numeric representation of the Unit object - */ - Unit(int level) { - this.level = level; - } - - @Override - public int getLevel() { - return this.level; - } - - @Override - public String getUnitString() { - switch (this) { - case Bit: - return "b"; - case Byte: - return "B"; - } - return null; - } - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/SignalStrengthLevel.java b/app/src/main/java/com/lcl/lclmeasurementtool/Utils/SignalStrengthLevel.java deleted file mode 100644 index 84a983c..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/SignalStrengthLevel.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.lcl.lclmeasurementtool.Utils; - -import android.graphics.Color; -import android.content.Context; - -import androidx.core.content.ContextCompat; - -import com.lcl.lclmeasurementtool.R; - -/** - * The SignalStrength represents the cellular signal strength - * received from the mobile network system. - */ -public enum SignalStrengthLevel { - NONE(0), // the signal strength is none or unknown - POOR(1), // the signal strength is poor - MODERATE(2), // the signal strength is moderate - GOOD(3), // the signal strength is good - GREAT(4); // the signal strength is great - - // the numerical equivalence of the signal strength - private final int levelCode; - - // constructor - SignalStrengthLevel(int levelCode) { - this.levelCode = levelCode; - } - - /** - * Initialize a SignalStrengthLevel based on input levelCode - * @param levelCode the input abstract representation of the Signal Strength; - * @throws IllegalArgumentException if the input levelCode is less than 0 or greater than 4. - * @return a SignalStrengthLevel Enum associated with the input levelCode. - */ - public static SignalStrengthLevel init(int levelCode) { - switch (levelCode) { - case 0: - return SignalStrengthLevel.NONE; - case 1: - return SignalStrengthLevel.POOR; - case 2: - return SignalStrengthLevel.MODERATE; - case 3: - return SignalStrengthLevel.GOOD; - case 4: - return SignalStrengthLevel.GREAT; - default: throw new IllegalArgumentException("Signal Strength levelCode should be >=0 and <= 4. Current value is " + levelCode); - } - } - - /** - * Get the numerical level code corresponding to the signal strength - * @return an integer associates with the signal strength - */ - public int getLevelCode() { - return this.levelCode; - } - - @Override - public String toString() { - return this.name() + " " + - "levelCode=" + levelCode; - } - - /** - * Retrieve the name of the signal strength. - * - * @return the string representation of the signal strength level. - */ - public String getName() { - switch (this) { - case NONE: - return "No Signal"; - case POOR: - return "Poor"; - case MODERATE: - return "Moderate"; - case GOOD: - return "Good"; - case GREAT: - return "Great"; - default: - break; - } - - return ""; - } - - /** - * Retrieve the color representation of the signal strength. - * @param context the context of the application/activity - * @return the color representation of the signal strength. - */ - public int getColor(Context context) { - switch (this) { - case GREAT: - return Color.GREEN; - case GOOD: - return ContextCompat.getColor(context, R.color.light_green); - case MODERATE: - return ContextCompat.getColor(context, R.color.orange); - case POOR: - return Color.RED; - case NONE: - return ContextCompat.getColor(context, R.color.light_gray); - default:break; - } - - return -1; - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/UIUtils.java b/app/src/main/java/com/lcl/lclmeasurementtool/Utils/UIUtils.java deleted file mode 100644 index b5a252e..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/UIUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.lcl.lclmeasurementtool.Utils; - -import android.app.AlertDialog; -import android.content.Context; -import android.content.DialogInterface; -import android.view.View; - -import com.google.android.material.snackbar.Snackbar; - -public class UIUtils { - - /** - * Create and show a system Alert Dialog with given content. - * - * @param context the current context of the application - * @param titleID the ID of the title string in string.xml - * @param messageID the ID of the message string in string.xml - * @param positiveMessageID the ID of the string of the positive action - * @param positiveListener the listener when the positive action is triggered - * @param negativeMessageID the ID of the string of the negative action - * @param negativeListener the listener when the negative action is triggered - */ - public static void showDialog(Context context, int titleID, final int messageID, - final int positiveMessageID, - DialogInterface.OnClickListener positiveListener, - final int negativeMessageID, - DialogInterface.OnClickListener negativeListener) { - new AlertDialog.Builder(context) - .setTitle(titleID) - .setMessage(messageID) - .setPositiveButton(positiveMessageID, positiveListener) - .setNegativeButton(negativeMessageID, negativeListener) - .show(); - } - - /** - * Create and show a system Snackbar with given content - * - * @param view the view on which the snack bar will be present - * @param textString the string of the text message - * @param actionStringId the ID of the name of the action - * @param listener the listener when the action is triggered - */ - public static void showSnackbar(View view, final String textString, final String actionStringId, - View.OnClickListener listener) { - Snackbar.make(view, - textString, - Snackbar.LENGTH_INDEFINITE) - .setAction(actionStringId, listener).show(); - } -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/UnitUtils.java b/app/src/main/java/com/lcl/lclmeasurementtool/Utils/UnitUtils.java deleted file mode 100644 index f793716..0000000 --- a/app/src/main/java/com/lcl/lclmeasurementtool/Utils/UnitUtils.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.lcl.lclmeasurementtool.Utils; - -/** - * A Utility class for unit strings - */ -public class UnitUtils { - - /** the signal strength unit */ - public static final String SIGNAL_STRENGTH_UNIT = "dBm"; - - /** the ping test unit */ - public static final String PING_UNIT = "ms"; -} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/constants/IperfConstants.kt b/app/src/main/java/com/lcl/lclmeasurementtool/constants/IperfConstants.kt new file mode 100644 index 0000000..8478357 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/constants/IperfConstants.kt @@ -0,0 +1,42 @@ +package com.lcl.lclmeasurementtool.constants + +import android.os.Build +import android.util.Base64 +import java.nio.charset.StandardCharsets + +@Deprecated(message = "This class is deprecated as Iperf is not supported in the measurement testbed") +class IperfConstants { + companion object { + const val IC_isDebug = true + + // the SSL key + // TODO: hide it + var IC_SSL_PK: String = """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwt8Pvsja7c6Co8nsyrCc + qCYz3liIEUYS1QaoMgefQHRUoIVVi8Gh7/ZAzu6+Jfl/b0qhIb9vgbdKhSYM7lfB + g2tkdH8tvdH7RzgfItp07+7j8YZd/XpDQKlOV4Ldv7rXhv/LrjlXGj4Zwq77CKdD + UsAl/MDl83v6NhusqvndxZRBCviEJ38C2H8axVmkjc/rVL8sWuqJ3w8qPuGkuNls + WElUA6cjjhG1NJMdzrMGGK9SkvtAwcUnNfgfyDvHml0Psaujwf8flhWI/cM42ZOU + SJCXtDOI5zoS+iijAkDfo8yQ0jMT8O1vYZqnX2ZErw8rcyQ9oLKkY2mHGFOxoZ6M + TQIDAQAB + -----END PUBLIC KEY----- + """.trimIndent() + + const val IC_test_username = "secrettestuser" + const val IC_test_password = "secrettestuser" + + const val IC_serverAddr = "othello-iperf.westus2.cloudapp.azure.com" + const val IC_serverPort = 40404 + + fun base64Encode(input: String): String { + val buffer = input.toByteArray(StandardCharsets.US_ASCII) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // return the correct encoding for iperf + java.util.Base64.getEncoder().encodeToString(buffer) + } else { + Base64.encodeToString(buffer, Base64.NO_WRAP) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/constants/NetworkConstants.kt b/app/src/main/java/com/lcl/lclmeasurementtool/constants/NetworkConstants.kt new file mode 100644 index 0000000..1b56d7e --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/constants/NetworkConstants.kt @@ -0,0 +1,23 @@ +package com.lcl.lclmeasurementtool.constants + +class NetworkConstants { + + companion object { + const val URL = "https://coverage.seattlecommunitynetwork.org/api/" + const val REGISTRATION_ENDPOINT = "register" + const val SIGNAL_ENDPOINT = "report_signal" + const val CONNECTIVITY_ENDPOINT = "report_measurement" + const val MEDIA_TYPE = "application/json" + + // NetworkTestViewModel Constants + const val PING_TAG = "PING" + const val IPERF_UP_TAG = "IPERF_UP" + const val IPERF_DOWN_TAG = "IPERF_DOWN" + const val WORKER_TAG = "backgroundTest" + const val IPERF_COUNTS_TAG = "TIMES" + const val IPERF_COUNTS = 5 + const val PING_TEST_ADDRESS = "google.com" + const val PING_TEST_ADDRESS_TAG = "ADDRESS" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/constants/README.md b/app/src/main/java/com/lcl/lclmeasurementtool/constants/README.md new file mode 100644 index 0000000..67243a7 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/constants/README.md @@ -0,0 +1,3 @@ +# Constants + +`Constants` directory contains constants used in various places across the project. \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/constants/SimCardConstants.kt b/app/src/main/java/com/lcl/lclmeasurementtool/constants/SimCardConstants.kt new file mode 100644 index 0000000..b5cbc00 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/constants/SimCardConstants.kt @@ -0,0 +1,17 @@ +package com.lcl.lclmeasurementtool.constants + +class SimCardConstants { + companion object { + /* The extra data for broacasting intent INTENT_ICC_STATE_CHANGE */ + const val INTENT_KEY_ICC_STATE = "ss" + + /* READY means ICC is ready to access */ + const val INTENT_VALUE_ICC_READY = "READY" + + /* IMSI means ICC IMSI is ready in property */ + const val INTENT_VALUE_ICC_IMSI = "IMSI" + + /* LOADED means all ICC records, including IMSI, are loaded */ + const val INTENT_VALUE_ICC_LOADED = "LOADED" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/database/README.md b/app/src/main/java/com/lcl/lclmeasurementtool/database/README.md new file mode 100644 index 0000000..4e0a057 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/database/README.md @@ -0,0 +1,8 @@ +# Database + +`Database` directory contains DB-related configuration and code. + +The project uses Android's Room DB as the underlying choice of database. `MeasurementResultDatabase` under the `DB` directory is a singleton used to connect to the databse and perform querying. + +Current the database stores two entities: Signal Strength and Connectivity Measurement. +To query the data, use the respective `ViewModel` or the `AbstractViewModel` class which performs the data querying using the respective `data access model(DAO)`. diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/database/dao/ConnectivityDao.kt b/app/src/main/java/com/lcl/lclmeasurementtool/database/dao/ConnectivityDao.kt new file mode 100644 index 0000000..5e439c7 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/database/dao/ConnectivityDao.kt @@ -0,0 +1,30 @@ +package com.lcl.lclmeasurementtool.database.dao + +import androidx.room.* +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel +import kotlinx.coroutines.flow.Flow + +@Dao +interface ConnectivityDao { + // CREATE + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(connectivity: ConnectivityReportModel) + + // READ + @Transaction + @Query("SELECT * FROM connectivity_table ORDER BY time_stamp DESC") + fun getAll(): Flow> + + @Transaction + @Query("SELECT * FROM connectivity_table where reported = false") + fun getAllNotReported(): List + + // WRITE + @Transaction + @Update + suspend fun updateReportStatus(connectivity: ConnectivityReportModel) + + // DELETE + @Query("DELETE FROM connectivity_table") + fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/database/dao/SignalStrengthDao.kt b/app/src/main/java/com/lcl/lclmeasurementtool/database/dao/SignalStrengthDao.kt new file mode 100644 index 0000000..8414ed6 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/database/dao/SignalStrengthDao.kt @@ -0,0 +1,31 @@ +package com.lcl.lclmeasurementtool.database.dao + +import androidx.room.* +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel +import kotlinx.coroutines.flow.Flow + +@Dao +interface SignalStrengthDao { + // CREATE + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(signalStrength: SignalStrengthReportModel) + + // READ + @Transaction + @Query("SELECT * FROM signal_strength_table ORDER BY time_stamp DESC") + fun getAll(): Flow> + + @Transaction + @Query("SELECT * FROM signal_strength_table where reported = false") + fun getAllNotReported(): List + + // WRITE + @Transaction + @Update + suspend fun updateReportStatus(signalStrength: SignalStrengthReportModel) + + // DELETE + @Query("DELETE FROM signal_strength_table") + fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/database/db/AppDatabase.kt b/app/src/main/java/com/lcl/lclmeasurementtool/database/db/AppDatabase.kt new file mode 100644 index 0000000..4edf7cb --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/database/db/AppDatabase.kt @@ -0,0 +1,31 @@ +package com.lcl.lclmeasurementtool.database.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import com.lcl.lclmeasurementtool.database.dao.ConnectivityDao +import com.lcl.lclmeasurementtool.database.dao.SignalStrengthDao +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel + +@Database(entities = [SignalStrengthReportModel::class, ConnectivityReportModel::class], version = 1) +abstract class AppDatabase: RoomDatabase() { + abstract fun signalStrengthDao(): SignalStrengthDao + abstract fun connectivityDao(): ConnectivityDao +// companion object { +// +// @Volatile +// private var INSTANCE: AppDatabase? = null +// +// fun getDatabase(context: Context): AppDatabase { +// return INSTANCE ?: synchronized(this) { +// val instance = Room +// .databaseBuilder(context, AppDatabase::class.java, "measurement_db") +// .build() +// INSTANCE = instance +// instance +// } +// } +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datasource/ConnectivityMonitorDataSource.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/ConnectivityMonitorDataSource.kt new file mode 100644 index 0000000..454287c --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/ConnectivityMonitorDataSource.kt @@ -0,0 +1,73 @@ +package com.lcl.lclmeasurementtool.datasource + +import android.content.Context +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import android.os.Build.VERSION_CODES +import android.util.Log +import androidx.core.content.getSystemService +import com.lcl.lclmeasurementtool.networking.NetworkMonitor +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import javax.inject.Inject + +class ConnectivityMonitorDataSource @Inject constructor( + @ApplicationContext private val context: Context) : NetworkMonitor{ + + override val isOnline: Flow = callbackFlow { + val connectivityManager = context.getSystemService() + + val callback = object: NetworkCallback() { + override fun onAvailable(network: Network) { + Log.d("ConnectivityMonitorDataSource", "available") +// channel.trySend(connectivityManager.isCurrentlyConnected()) + channel.trySend(true) + } + + override fun onLost(network: Network) { + Log.d("ConnectivityMonitorDataSource", "lost") +// channel.trySend(connectivityManager.isCurrentlyConnected()) + channel.trySend(false) + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + val hasWIFI = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + Log.d("ConnectivityMonitorDataSource", "changed") + Log.d("ConnectivityMonitorDataSource", "hasWIFI: $hasWIFI") + channel.trySend(!hasWIFI) +// channel.trySend(connectivityManager.isCurrentlyConnected()) + } + } + + connectivityManager?.registerNetworkCallback( + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) +// .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .build(), + callback + ) + + channel.trySend(connectivityManager.isCurrentlyConnected()) + + awaitClose { + connectivityManager?.unregisterNetworkCallback(callback) + } + }.conflate() + + @Suppress("DEPRECATION") + private fun ConnectivityManager?.isCurrentlyConnected() = when(this) { + null -> false + else -> activeNetwork?.let(::getNetworkCapabilities) + ?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datasource/LocationDataSource.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/LocationDataSource.kt new file mode 100644 index 0000000..3cfb165 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/LocationDataSource.kt @@ -0,0 +1,68 @@ +package com.lcl.lclmeasurementtool.datasource + +import android.annotation.SuppressLint +import android.content.Context +import android.location.Location +import com.google.android.gms.location.* +import com.google.android.gms.tasks.CancellationTokenSource +import com.google.android.gms.tasks.OnSuccessListener +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import com.lcl.lclmeasurementtool.location.LocationService +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class LocationDataSource @Inject constructor( + @ApplicationContext private val context: Context +): LocationService { + companion object { + const val LOCATION_INTERVAL = 10000L + const val TAG = "LocationDataSource" + } + + private val fusedLocationProviderClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + private val currentLocationRequest = CurrentLocationRequest.Builder().setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY).setDurationMillis(LOCATION_INTERVAL).build() + + + @SuppressLint("MissingPermission") + override fun lastLocation() = callbackFlow { + if (!XXPermissions.isGranted(context, Permission.ACCESS_COARSE_LOCATION, Permission.ACCESS_FINE_LOCATION)) { + XXPermissions + .with(context) + .permission(Permission.ACCESS_COARSE_LOCATION, Permission.ACCESS_FINE_LOCATION) + .request { permissions, allGranted -> + if (!allGranted) { + TODO("Show message to the user") + } + } + } + + val callback = OnSuccessListener { task -> trySend(task) } + + fusedLocationProviderClient + .lastLocation + .addOnSuccessListener(callback) + + awaitClose { + + } + } + + @SuppressLint("MissingPermission") + fun getCurrentLocation() = callbackFlow { + if (!XXPermissions.isGranted(context, Permission.ACCESS_COARSE_LOCATION, Permission.ACCESS_FINE_LOCATION)) { + XXPermissions + .with(context) + .permission(Permission.ACCESS_COARSE_LOCATION, Permission.ACCESS_FINE_LOCATION) + .request { permissions, allGranted -> + if (!allGranted) { + TODO("Show message to the user") + } + } + } + val callback = OnSuccessListener { task -> trySend(task) } + fusedLocationProviderClient.getCurrentLocation(currentLocationRequest, CancellationTokenSource().token).addOnSuccessListener(callback) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datasource/PreferencesDataSource.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/PreferencesDataSource.kt new file mode 100644 index 0000000..1d692b3 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/PreferencesDataSource.kt @@ -0,0 +1,86 @@ +package com.lcl.lclmeasurementtool.datasource + +import android.content.Context +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStore +import com.google.protobuf.ByteString +import com.lcl.lclmeasurementtool.datastore.UserPreferences +import com.lcl.lclmeasurementtool.model.datamodel.UserData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException +import javax.inject.Inject + +val Context.dataStore by preferencesDataStore(name = "settings") + +class PreferencesDataSource @Inject constructor( + private val userPreferences: DataStore) { + + val userData = userPreferences.data.map { + UserData( + deviceID = it.deviceId, + skT = it.skT, + hPKR = it.hPkr, + showData = it.showData, + loggedIn = it.loggedIn, // !(it.skT.isEmpty || it.hPkr.isEmpty) + R = it.r + ) + } + + suspend fun toggleShowData(showData: Boolean) { + try { + userPreferences.updateData { + it.toBuilder().setShowData(showData).build() + } + } catch (e: IOException) { + // TODO: alert to user + throw e + } + } + + suspend fun setDeviceID(newDeviceID: String) { + try { + userPreferences.updateData { + it.toBuilder().setDeviceId(newDeviceID).build() + } + } catch (e: IOException) { + // TODO: alert to user + throw e + } + } + + suspend fun getDeviceID() { + + } + + suspend fun setR(R: ByteString) { + userPreferences.updateData { + it.toBuilder().setR(R).build() + } + } + + suspend fun setKeys(hPKR: ByteString, skT: ByteString) { + userPreferences.updateData { + if (!it.hPkr.isEmpty || !it.skT.isEmpty) { + logout() + } + Log.d("PreferenceDataSource", "set login to true") + it.toBuilder().setHPkr(hPKR).setSkT(skT).setLoggedIn(true).build() + } + } + + suspend fun logout() { + try { + userPreferences.updateData { + Log.d("PreferenceDataSource", "set login to false") + it.toBuilder().setLoggedIn(false).clearHPkr().clearSkT().clearDeviceId().build() + } + } catch (e: IOException) { + // TODO: alert to user + throw e + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datasource/SignalStrengthDataSource.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/SignalStrengthDataSource.kt new file mode 100644 index 0000000..eb70c32 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/SignalStrengthDataSource.kt @@ -0,0 +1,91 @@ +package com.lcl.lclmeasurementtool.datasource + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.os.Build.VERSION_CODES.S +import android.telephony.* +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import androidx.core.content.getSystemService +import com.lcl.lclmeasurementtool.telephony.SignalStrengthMonitor +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import javax.inject.Inject + +class SignalStrengthDataSource @Inject constructor( + @ApplicationContext private val context: Context +): SignalStrengthMonitor { + + private val telephonyManager = context.getSystemService() + + @SuppressLint("MissingPermission") + override fun getCellID(): String { + return when (val info = telephonyManager?.allCellInfo?.firstOrNull()) { + null -> "unknown" + is CellInfoGsm -> { + info.cellIdentity.cid.toString() + } + is CellInfoLte -> { + info.cellIdentity.ci.toString() + } + + is CellInfoCdma -> { + val cellIdentity = info.cellIdentity + String.format( + "%04x%04x%04x", + cellIdentity.systemId, + cellIdentity.networkId, + cellIdentity.basestationId + ) + } + + is CellInfoWcdma -> { + info.cellIdentity.cid.toString() + } + else -> "unknown" + } + } + + @OptIn(FlowPreview::class) + override val signalStrength = callbackFlow { +// val telephonyManager = context.getSystemService() + val executor = Executors.newSingleThreadExecutor() + + if (Build.VERSION.SDK_INT >= 31) { + val callback = object: TelephonyCallback(), TelephonyCallback.SignalStrengthsListener { + override fun onSignalStrengthsChanged(s: SignalStrength) { + channel.trySend(s) + } + } + + telephonyManager?.registerTelephonyCallback(executor, callback) + + awaitClose { + telephonyManager?.unregisterTelephonyCallback(callback) + executor.shutdown() + } + } else { + @Suppress("OVERRIDE_DEPRECATION") + val callback = object : PhoneStateListener(executor) { + override fun onSignalStrengthsChanged(s: SignalStrength) { + super.onSignalStrengthsChanged(s) + channel.trySend(s) + } + } + + telephonyManager?.listen(callback, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS) + + awaitClose { + telephonyManager?.listen(callback, 0) + executor.shutdown() + } + } + }.conflate().sample(5000L).distinctUntilChanged() +} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datasource/SimStateMonitorDataSource.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/SimStateMonitorDataSource.kt new file mode 100644 index 0000000..9142c33 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datasource/SimStateMonitorDataSource.kt @@ -0,0 +1,68 @@ +package com.lcl.lclmeasurementtool.datasource + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.telephony.TelephonyManager +import android.util.Log +import androidx.core.content.getSystemService +import com.lcl.lclmeasurementtool.constants.SimCardConstants +import com.lcl.lclmeasurementtool.networking.SimStateMonitor +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import javax.inject.Inject + +class SimStateMonitorDataSource @Inject constructor( + @ApplicationContext private val context: Context +): SimStateMonitor { + companion object { + const val SIM_STATE_CHANGED = "android.intent.action.SIM_STATE_CHANGED" + } + + override val isSimCardInserted: Flow = callbackFlow { + val intentFilter = IntentFilter() + val telephonyManager = context.getSystemService() + intentFilter.addAction(SIM_STATE_CHANGED) + + val receiver = object: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == SIM_STATE_CHANGED) { + val states = intent.getStringExtra(SimCardConstants.INTENT_KEY_ICC_STATE) + if (states != SimCardConstants.INTENT_VALUE_ICC_LOADED && + states != SimCardConstants.INTENT_VALUE_ICC_IMSI && + states != SimCardConstants.INTENT_VALUE_ICC_READY) { + Log.d("SimStateDataSource", "simState doesnt match: $states") + channel.trySend(false) + } else { + channel.trySend(true) + } + } + } + } + + context.registerReceiver(receiver, intentFilter) + + channel.trySend(telephonyManager.isSimCardInserted()) + + awaitClose { + context.unregisterReceiver(receiver) + } + }.conflate() + + private fun TelephonyManager?.isSimCardInserted() = when(this) { + null -> { + Log.d("SimStateDataSource", "simState not initialized") + false + } + else -> { + Log.d("SimStateDataSource", "simState is $simState") + simState == TelephonyManager.SIM_STATE_READY + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datastore/DataStoreModule.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/DataStoreModule.kt new file mode 100644 index 0000000..e51b03f --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/DataStoreModule.kt @@ -0,0 +1,35 @@ +package com.lcl.lclmeasurementtool.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.datastore.preferences.core.Preferences +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + + @Provides + @Singleton + fun providesUserPreferenceDataStore( + @ApplicationContext context: Context, + @Dispatcher(LCLDispatchers.IO) ioDispatcher: CoroutineDispatcher, + userPreferencesSerializer: UserPreferencesSerializer + ) : DataStore = + DataStoreFactory.create( + serializer = userPreferencesSerializer, + scope = CoroutineScope( ioDispatcher + SupervisorJob() ), + ) { + context.dataStoreFile("user_preferences.pb") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datastore/Dispatcher.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/Dispatcher.kt new file mode 100644 index 0000000..5770fd5 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/Dispatcher.kt @@ -0,0 +1,12 @@ +package com.lcl.lclmeasurementtool.datastore + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.RUNTIME) +annotation class Dispatcher(val dispatcher: LCLDispatchers) + +enum class LCLDispatchers { + IO +} + diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datastore/DispatcherModule.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/DispatcherModule.kt new file mode 100644 index 0000000..e881318 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/DispatcherModule.kt @@ -0,0 +1,16 @@ +package com.lcl.lclmeasurementtool.datastore + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + +@Module +@InstallIn(SingletonComponent::class) +object DispatchersModule { + @Provides + @Dispatcher(LCLDispatchers.IO) + fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/datastore/UserPreferencesSerializer.kt b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/UserPreferencesSerializer.kt new file mode 100644 index 0000000..0b3a437 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/datastore/UserPreferencesSerializer.kt @@ -0,0 +1,27 @@ +package com.lcl.lclmeasurementtool.datastore + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.protobuf.InvalidProtocolBufferException +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject + +class UserPreferencesSerializer @Inject constructor() : Serializer { + override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): UserPreferences = + try { + // readFrom is already called on the data store background thread + @Suppress("BlockingMethodInNonBlockingContext") + UserPreferences.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + + override suspend fun writeTo(t: UserPreferences, output: OutputStream) { + // writeTo is already called on the data store background thread + @Suppress("BlockingMethodInNonBlockingContext") + t.writeTo(output) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/errors/DecoderException.java b/app/src/main/java/com/lcl/lclmeasurementtool/errors/DecoderException.java new file mode 100644 index 0000000..ad96a46 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/errors/DecoderException.java @@ -0,0 +1,82 @@ +// Imported from apache commons-codec. +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.lcl.lclmeasurementtool.errors; + +public class DecoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public DecoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + */ + public DecoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final Throwable cause) { + super(cause); + } +} + diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/errors/EncoderException.java b/app/src/main/java/com/lcl/lclmeasurementtool/errors/EncoderException.java new file mode 100644 index 0000000..7fcc708 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/errors/EncoderException.java @@ -0,0 +1,86 @@ +// Imported from apache commons-codec. +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.lcl.lclmeasurementtool.errors; + +/** + * EncoderException from apache commons + */ +public class EncoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public EncoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message + * a useful message relating to the encoder specific error. + */ + public EncoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + *

+ * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final Throwable cause) { + super(cause); + } +} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt new file mode 100644 index 0000000..7b6ff41 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabCallback.kt @@ -0,0 +1,10 @@ +package com.lcl.lclmeasurementtool.features.mlab + +import net.measurementlab.ndt7.android.NDTTest +import net.measurementlab.ndt7.android.models.ClientResponse + +interface MLabCallback { + fun onDownloadProgress(clientResponse: ClientResponse) + fun onUploadProgress(clientResponse: ClientResponse) + fun onFinish(clientResponse: ClientResponse?, error: Throwable?, testType: NDTTest.TestType) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt new file mode 100644 index 0000000..6273c80 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabResult.kt @@ -0,0 +1,16 @@ +package com.lcl.lclmeasurementtool.features.mlab + +import net.measurementlab.ndt7.android.NDTTest + +data class MLabResult( + val speed: String?, + val type: NDTTest.TestType, + val errorMsg: String?, + val status: MLabTestStatus +) + +enum class MLabTestStatus { + RUNNING, + FINISHED, + ERROR +} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt new file mode 100644 index 0000000..1e79caf --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/mlab/MLabRunner.kt @@ -0,0 +1,84 @@ +package com.lcl.lclmeasurementtool.features.mlab + +import android.util.Log +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import net.measurementlab.ndt7.android.NDTTest +import net.measurementlab.ndt7.android.models.ClientResponse +import net.measurementlab.ndt7.android.utils.DataConverter +import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit + +class MLabRunner(httpClient: OkHttpClient, private val callback: MLabCallback): NDTTest(httpClient) { + override fun onDownloadProgress(clientResponse: ClientResponse) { + super.onDownloadProgress(clientResponse) + callback.onDownloadProgress(clientResponse) + } + + override fun onUploadProgress(clientResponse: ClientResponse) { + super.onUploadProgress(clientResponse) + callback.onUploadProgress(clientResponse) + } + + override fun onFinished( + clientResponse: ClientResponse?, + error: Throwable?, + testType: TestType + ) { + super.onFinished(clientResponse, error, testType) + callback.onFinish(clientResponse, error, testType) + } + + companion object { + const val TAG = "MLabRunner" + + fun runTest(testType: TestType) = callbackFlow { + val callback = object : MLabCallback { + override fun onDownloadProgress(clientResponse: ClientResponse) { + val speed = DataConverter.convertToMbps(clientResponse) + Log.d(TAG, "client download is $speed") + channel.trySend(MLabResult(speed, TestType.DOWNLOAD, null, MLabTestStatus.RUNNING)) + } + + override fun onUploadProgress(clientResponse: ClientResponse) { + val speed = DataConverter.convertToMbps(clientResponse) + Log.d(TAG, "client upload is $speed") + channel.trySend(MLabResult(speed, TestType.UPLOAD, null, MLabTestStatus.RUNNING)) + } + + override fun onFinish( + clientResponse: ClientResponse?, + error: Throwable?, + testType: TestType + ) { + if (clientResponse != null) { + Log.d(TAG, "client finish test $testType") + channel.trySend(MLabResult(DataConverter.convertToMbps(clientResponse), testType, null, MLabTestStatus.FINISHED)) + } else { + channel.trySend(MLabResult(null, testType, error?.message, MLabTestStatus.ERROR)) + } + + if (testType == TestType.UPLOAD) channel.close() + } + } + + val testRunner = MLabRunner(createHttpClient(), callback) + + testRunner.startTest(testType) + Log.d(TAG, "right after the testRunner.startTest(testType) call") + awaitClose { + Log.d(TAG, "channel is about to close ...") + testRunner.stopTest() + } + } + + private fun createHttpClient(connectTimeout: Long = 10, readTimeout: Long = 10, writeTimeout: Long = 10): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(connectTimeout, TimeUnit.SECONDS) + .readTimeout(readTimeout, TimeUnit.SECONDS) + .writeTimeout(writeTimeout, TimeUnit.SECONDS) + .build() + } + } +} + diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/Ping.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/Ping.kt new file mode 100644 index 0000000..b9f3e53 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/Ping.kt @@ -0,0 +1,47 @@ +package com.lcl.lclmeasurementtool.features.ping + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn + +class Ping { + companion object { + suspend fun start(address: String, times: Int = 5, timeout: Long) : PingResult { + if (address.isEmpty()) { + throw IllegalArgumentException() + } + + if (times < 0) { + throw IllegalArgumentException() + } + + if (timeout < 0) { + throw IllegalArgumentException() + } + + return PingUtil.doPing(address, times, timeout) + } + + suspend fun cancellableStart(address: String, times: Int = 5, timeout: Long) = flow { + if (address.isEmpty()) { + throw IllegalArgumentException() + } + + if (times < 0) { + throw IllegalArgumentException() + } + + if (timeout < 0) { + throw IllegalArgumentException() + } + + emit(PingUtil.doPing(address, times, timeout)) + }.flowOn(Dispatchers.IO).cancellable() + + suspend fun fakeStart(address: String, times: Int = 5, timeout: Long) = + flowOf(PingResult("0", "4.5", "5.1", "6.2", "0.34", PingError(PingErrorCase.OK, null))) + .flowOn(Dispatchers.IO).cancellable() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingError.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingError.kt new file mode 100644 index 0000000..bfaa3d1 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingError.kt @@ -0,0 +1,14 @@ +package com.lcl.lclmeasurementtool.features.ping + +data class PingError( + val code: PingErrorCase, + val message: String? = null +) + +enum class PingErrorCase(val exitCode: Int) { + OK(0), + IO(1), + PARSING(2), + OTHER(3), + CANCELLED(4) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingResult.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingResult.kt new file mode 100644 index 0000000..5c52c1d --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingResult.kt @@ -0,0 +1,10 @@ +package com.lcl.lclmeasurementtool.features.ping + +data class PingResult( + val numLoss: String? = null, + val min: String? = null, + val avg: String? = null, + val max: String? = null, + val mdev: String? = null, + val error: PingError +) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingUtil.kt b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingUtil.kt new file mode 100644 index 0000000..e61c493 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/features/ping/PingUtil.kt @@ -0,0 +1,75 @@ +package com.lcl.lclmeasurementtool.features.ping + +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader + +class PingUtil { + companion object { + const val TAG = "PING" + + suspend fun doPing(address: String, times: Int, timeout: Long) : PingResult { + val runtime: Runtime = Runtime.getRuntime() + + // execute ping command + val command = "/system/bin/ping -c $times -W $timeout $address" + + try { + val process = withContext(Dispatchers.IO) { + runtime.exec(command) + } + Log.d(TAG, "============") + Log.d(TAG, "Ping starts: $address") + Log.d(TAG, "============") + val exitCode = withContext(Dispatchers.IO) { + process.waitFor() + } + Log.d(TAG, "exit code is: $exitCode") + + val pingResult: PingResult + + when(exitCode) { + 0 -> { + val reader = BufferedReader(InputStreamReader(process.inputStream)) + val line: String = reader.use { + it.readText().trimIndent() + } + Log.d(TAG, "result:\n$line") + + val regex = "(\\d+)% packet loss.+rtt.+= (\\d*.?\\d+)/(\\d*.?\\d+)/(\\d*.?\\d+)/(\\d*.?\\d+)".toRegex( + setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL) + ) + val match = regex.find(line)!! + val (numLoss, min, avg, max, mdev) = match.destructured + Log.d(TAG, "$numLoss, $min, $avg, $max, $mdev") + pingResult = PingResult( + numLoss = numLoss, + min = min, + avg = avg, + max = max, + mdev = mdev, + error = PingError(code = PingErrorCase.OK) + ) + } + else -> { + val err = process.errorStream.bufferedReader().use { it.readText() } + pingResult = PingResult(error = PingError(code = PingErrorCase.IO, message = err)) + Log.d(TAG, "error: $err") + } + } + + process.destroy() + return pingResult + } catch (e: IOException) { + return PingResult(error = PingError(PingErrorCase.IO, e.message)) + } catch (e: IndexOutOfBoundsException) { + return PingResult(error = PingError(PingErrorCase.PARSING, e.message)) + } catch (e: Exception) { + return PingResult(error = PingError(PingErrorCase.OTHER, e.message)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/location/LocationService.kt b/app/src/main/java/com/lcl/lclmeasurementtool/location/LocationService.kt new file mode 100644 index 0000000..e645cb5 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/location/LocationService.kt @@ -0,0 +1,9 @@ +package com.lcl.lclmeasurementtool.location + +import android.location.Location +import kotlinx.coroutines.flow.Flow + +interface LocationService { +// val currentLocation: Flow + fun lastLocation(): Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/BaseMeasureDataModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/BaseMeasureDataModel.kt new file mode 100644 index 0000000..a41b9a3 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/BaseMeasureDataModel.kt @@ -0,0 +1,16 @@ +package com.lcl.lclmeasurementtool.model.datamodel + +@kotlinx.serialization.Serializable +sealed interface BaseMeasureDataModel { + var latitude: Double + + var longitude: Double + + var timestamp: String + + var cellId: String + + var deviceId: String + + var reported: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/ConnectivityReportModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/ConnectivityReportModel.kt new file mode 100644 index 0000000..6c2f696 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/ConnectivityReportModel.kt @@ -0,0 +1,22 @@ +package com.lcl.lclmeasurementtool.model.datamodel + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.SerialName +import kotlinx.serialization.Transient + +@kotlinx.serialization.Serializable +@Entity(tableName = "connectivity_table") +data class ConnectivityReportModel constructor( + @ColumnInfo(name = "latitude") @SerialName("latitude") override var latitude: Double, + @ColumnInfo(name = "longitude") @SerialName("longitude") override var longitude: Double, + @PrimaryKey @ColumnInfo(name = "time_stamp") @SerialName("timestamp") override var timestamp: String, + @SerialName("cell_id") override var cellId: String, + @SerialName("device_id") override var deviceId: String, + @ColumnInfo(name = "upload_speed") @SerialName("upload_speed") var uploadSpeed: Double, + @ColumnInfo(name = "download_speed") @SerialName("download_speed") var downloadSpeed: Double, + @ColumnInfo(name = "ping") @SerialName("ping") var ping: Double, + @ColumnInfo(name = "package_loss") @SerialName("package_loss") var packetLoss: Double, + @ColumnInfo(name = "reported") @Transient override var reported: Boolean = false +) : BaseMeasureDataModel diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/MeasurementReportModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/MeasurementReportModel.kt new file mode 100644 index 0000000..d3758e1 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/MeasurementReportModel.kt @@ -0,0 +1,10 @@ +package com.lcl.lclmeasurementtool.model.datamodel + +import kotlinx.serialization.SerialName + +@kotlinx.serialization.Serializable +data class MeasurementReportModel( + @SerialName("sigma_m") var sigmaM: String, + @SerialName("h_pkr") var hPKR: String, + @SerialName("M") var M: String, + @SerialName("show_data") var showData: Boolean) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/QRCodeKeysModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/QRCodeKeysModel.kt new file mode 100644 index 0000000..f2764aa --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/QRCodeKeysModel.kt @@ -0,0 +1,9 @@ +package com.lcl.lclmeasurementtool.model.datamodel + +import kotlinx.serialization.SerialName + +@kotlinx.serialization.Serializable +data class QRCodeKeysModel constructor( + @SerialName("sigma_t") var sigmaT: String, + @SerialName("sk_t") var skT: String, + @SerialName("pk_a") var pk_a: String) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/RegistrationModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/RegistrationModel.kt new file mode 100644 index 0000000..551bffa --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/RegistrationModel.kt @@ -0,0 +1,9 @@ +package com.lcl.lclmeasurementtool.model.datamodel + +import kotlinx.serialization.SerialName + +@kotlinx.serialization.Serializable +data class RegistrationModel( + @SerialName("sigma_r") var sigmaR: String, + @SerialName("h") var h: String, + @SerialName("R") var R: String) diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/SignalStrengthReportModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/SignalStrengthReportModel.kt new file mode 100644 index 0000000..e0c3df6 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/SignalStrengthReportModel.kt @@ -0,0 +1,20 @@ +package com.lcl.lclmeasurementtool.model.datamodel + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.serialization.SerialName +import kotlinx.serialization.Transient + +@Entity(tableName = "signal_strength_table") +@kotlinx.serialization.Serializable +data class SignalStrengthReportModel( + @ColumnInfo(name = "latitude") @SerialName("latitude") override var latitude: Double, + @ColumnInfo(name = "longitude") @SerialName("longitude") override var longitude: Double, + @PrimaryKey @ColumnInfo(name = "time_stamp") @SerialName("timestamp") override var timestamp: String, + @SerialName("cell_id") override var cellId: String, + @SerialName("device_id") override var deviceId: String, + @SerialName("dbm") @ColumnInfo(name = "signal_strength") var dbm: Int, + @SerialName("level_code") @ColumnInfo(name = "signal_strength_level") var levelCode: Int, + @ColumnInfo("reported") @Transient override var reported: Boolean = false +) : BaseMeasureDataModel diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/UserData.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/UserData.kt new file mode 100644 index 0000000..731f89a --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/datamodel/UserData.kt @@ -0,0 +1,12 @@ +package com.lcl.lclmeasurementtool.model.datamodel + +import com.google.protobuf.ByteString + +data class UserData( + val deviceID: String, + val showData: Boolean, + val loggedIn: Boolean, + val hPKR: ByteString, + val skT: ByteString, + val R: ByteString +) \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/ConnectivityRepository.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/ConnectivityRepository.kt new file mode 100644 index 0000000..bd05b1a --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/ConnectivityRepository.kt @@ -0,0 +1,49 @@ +package com.lcl.lclmeasurementtool.model.repository + +import android.util.Log +import com.lcl.lclmeasurementtool.database.dao.ConnectivityDao +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel +import com.lcl.lclmeasurementtool.util.Synchronizer +import com.lcl.lclmeasurementtool.util.prepareReportData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class ConnectivityRepository @Inject constructor( + private val connectivityDao: ConnectivityDao, + private val networkApi: NetworkApiRepository, + private val userDataRepository: UserDataRepository, +): HistoryDataRepository { + override fun getAll(): Flow> = connectivityDao.getAll() + + override suspend fun update(data: ConnectivityReportModel) = connectivityDao.updateReportStatus(data) + override suspend fun insert(data: ConnectivityReportModel) = connectivityDao.insert(data) + + override suspend fun syncWith(synchronizer: Synchronizer) = synchronizer.syncData { + val toReportList = connectivityDao.getAllNotReported() + if (toReportList.isEmpty()) { + Log.d(TAG, "no outstanding connectivity reports") + return@syncData + } + + toReportList.asFlow() + .flowOn(Dispatchers.IO) + .combine(userDataRepository.userData) { connectivity, preference -> + Log.d(TAG, "upload worker will upload $connectivity") + val reportString = prepareReportData(connectivity, preference) + networkApi.uploadConnectivity(reportString) + } + .catch { + Log.d(TAG, "upload worker encounter $it when uploading connectivity") + throw it + } + .onCompletion { + Log.d(TAG, "finish uploading all unreported connectivity data") + } + .collect() + } + + companion object { + const val TAG = "ConnectivityRepository" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/HistoryDataRepository.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/HistoryDataRepository.kt new file mode 100644 index 0000000..899fc75 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/HistoryDataRepository.kt @@ -0,0 +1,10 @@ +package com.lcl.lclmeasurementtool.model.repository + +import com.lcl.lclmeasurementtool.util.Syncable +import kotlinx.coroutines.flow.Flow + +interface HistoryDataRepository: Syncable { + fun getAll(): Flow> + suspend fun insert(data: T) + suspend fun update(data: T) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LCLApiRepository.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LCLApiRepository.kt new file mode 100644 index 0000000..b3f61df --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LCLApiRepository.kt @@ -0,0 +1,15 @@ +package com.lcl.lclmeasurementtool.model.repository + +import com.lcl.lclmeasurementtool.networking.RetrofitLCLNetwork +import okhttp3.ResponseBody +import retrofit2.Response +import javax.inject.Inject + +class LCLApiRepository @Inject constructor( + private val dataSource: RetrofitLCLNetwork +): NetworkApiRepository { + + override suspend fun register(registration: String): ResponseBody = dataSource.register(registration) + override suspend fun uploadConnectivity(connectivityReportModel: String): ResponseBody = dataSource.uploadConnectivity(connectivityReportModel) + override suspend fun uploadSignalStrength(signalStrengthReportModel: String): ResponseBody = dataSource.uploadSignalStrength(signalStrengthReportModel) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LocalUserDataRepository.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LocalUserDataRepository.kt new file mode 100644 index 0000000..6b699c4 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/LocalUserDataRepository.kt @@ -0,0 +1,21 @@ +package com.lcl.lclmeasurementtool.model.repository + +import com.google.protobuf.ByteString +import com.lcl.lclmeasurementtool.datasource.PreferencesDataSource +import com.lcl.lclmeasurementtool.model.datamodel.UserData +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class LocalUserDataRepository @Inject constructor( + private val preferenceDataSource: PreferencesDataSource + ) : UserDataRepository { + override val userData: Flow = preferenceDataSource.userData + override suspend fun toggleShowData(showData: Boolean) = preferenceDataSource.toggleShowData(showData) + + override suspend fun setDeviceID(newDeviceID: String) = preferenceDataSource.setDeviceID(newDeviceID) + + override suspend fun setKeys(hPKR: ByteString, skT: ByteString) = preferenceDataSource.setKeys(hPKR, skT) + override suspend fun setR(R: ByteString) = preferenceDataSource.setR(R) + override suspend fun logout() = preferenceDataSource.logout() + +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/NetworkApiRepository.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/NetworkApiRepository.kt new file mode 100644 index 0000000..8cb0bb2 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/NetworkApiRepository.kt @@ -0,0 +1,11 @@ +package com.lcl.lclmeasurementtool.model.repository + +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.Body + +interface NetworkApiRepository { + suspend fun register(@Body registration: String): ResponseBody + suspend fun uploadSignalStrength(@Body signalStrengthReportModel: String): ResponseBody + suspend fun uploadConnectivity(@Body connectivityReportModel: String): ResponseBody +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/SignalStrengthRepository.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/SignalStrengthRepository.kt new file mode 100644 index 0000000..3389312 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/SignalStrengthRepository.kt @@ -0,0 +1,48 @@ +package com.lcl.lclmeasurementtool.model.repository + +import android.util.Log +import com.lcl.lclmeasurementtool.database.dao.SignalStrengthDao +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel +import com.lcl.lclmeasurementtool.util.Synchronizer +import com.lcl.lclmeasurementtool.util.prepareReportData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class SignalStrengthRepository @Inject constructor( + private val signalStrengthDao: SignalStrengthDao, + private val networkApi: NetworkApiRepository, + private val userDataRepository: UserDataRepository, +): HistoryDataRepository { + override fun getAll(): Flow> = signalStrengthDao.getAll() + override suspend fun update(data: SignalStrengthReportModel) = signalStrengthDao.updateReportStatus(data) + override suspend fun insert(data: SignalStrengthReportModel) = signalStrengthDao.insert(data) + + override suspend fun syncWith(synchronizer: Synchronizer) = synchronizer.syncData { + val toReportList = signalStrengthDao.getAllNotReported() + if (toReportList.isEmpty()) { + Log.d(TAG, "no outstanding signal strength reports") + return@syncData + } + + toReportList.asFlow() + .flowOn(Dispatchers.IO) + .combine(userDataRepository.userData) { signalStrength, preference -> + Log.d(TAG, "upload worker will upload $signalStrength") + val reportString = prepareReportData(signalStrength, preference) + networkApi.uploadSignalStrength(reportString) + } + .catch { + Log.d(TAG, "upload worker encounter the following exception when uploading outstanding signal strength report") + throw it + } + .onCompletion { + Log.d(TAG, "finish uploading all unreported signal strength data") + } + .collect() + } + + companion object { + const val TAG = "SignalStrengthRepository" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/UserDataRepository.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/UserDataRepository.kt new file mode 100644 index 0000000..21bf8a2 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/repository/UserDataRepository.kt @@ -0,0 +1,14 @@ +package com.lcl.lclmeasurementtool.model.repository + +import com.google.protobuf.ByteString +import com.lcl.lclmeasurementtool.model.datamodel.UserData +import kotlinx.coroutines.flow.Flow + +interface UserDataRepository { + val userData: Flow + suspend fun toggleShowData(showData: Boolean) + suspend fun setDeviceID(newDeviceID: String) + suspend fun setKeys(hPKR: ByteString, skT: ByteString) + suspend fun setR(R: ByteString) + suspend fun logout() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/ConnectivityViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/ConnectivityViewModel.kt new file mode 100644 index 0000000..87cd05b --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/ConnectivityViewModel.kt @@ -0,0 +1,42 @@ +package com.lcl.lclmeasurementtool.model.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel +import com.lcl.lclmeasurementtool.model.repository.ConnectivityRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ConnectivityViewModel @Inject constructor( + private val measurementsRepository: ConnectivityRepository +): ViewModel() { + fun insert(data: ConnectivityReportModel) { + viewModelScope.launch { + measurementsRepository.insert(data) + } + } + + val dataFlow: StateFlow = + measurementsRepository + .getAll() + .map(ConnectivityUiState::Success) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = ConnectivityUiState.Loading + ) +} + + +sealed interface ConnectivityUiState { + data class Success(val connectivities: List) : ConnectivityUiState + object Error : ConnectivityUiState + object Loading : ConnectivityUiState +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SettingsViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SettingsViewModel.kt new file mode 100644 index 0000000..860582b --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SettingsViewModel.kt @@ -0,0 +1,41 @@ +package com.lcl.lclmeasurementtool.model.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lcl.lclmeasurementtool.model.repository.UserDataRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val userDataRepository: UserDataRepository, +) : ViewModel() { + + val shouldShowData: StateFlow = userDataRepository + .userData + .map { it.showData } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = false + ) + + + fun toggleShowData(showData: Boolean) { + viewModelScope.launch { + userDataRepository.toggleShowData(showData = showData) + } + } + + fun logout() { + viewModelScope.launch { + userDataRepository.logout() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SignalStrengthViewModel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SignalStrengthViewModel.kt new file mode 100644 index 0000000..4298548 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/model/viewmodels/SignalStrengthViewModel.kt @@ -0,0 +1,41 @@ +package com.lcl.lclmeasurementtool.model.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel +import com.lcl.lclmeasurementtool.model.repository.SignalStrengthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SignalStrengthViewModel @Inject constructor( + private val measurementRepository: SignalStrengthRepository +): ViewModel() { + + fun insert(data: SignalStrengthReportModel) { + viewModelScope.launch { + measurementRepository.insert(data) + } + } + + val dataFlow: StateFlow = + measurementRepository + .getAll() + .map(SignalStrengthUiState::Success) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = SignalStrengthUiState.Loading + ) +} + +sealed interface SignalStrengthUiState { + data class Success(val signalStrengths: List) : SignalStrengthUiState + object Error : SignalStrengthUiState + object Loading : SignalStrengthUiState +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/modules/DaosModule.kt b/app/src/main/java/com/lcl/lclmeasurementtool/modules/DaosModule.kt new file mode 100644 index 0000000..36d3509 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/modules/DaosModule.kt @@ -0,0 +1,22 @@ +package com.lcl.lclmeasurementtool.modules + +import com.lcl.lclmeasurementtool.database.dao.ConnectivityDao +import com.lcl.lclmeasurementtool.database.dao.SignalStrengthDao +import com.lcl.lclmeasurementtool.database.db.AppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object DaosModule { + + @Provides + fun providesSignalStrengthDao(database: AppDatabase): SignalStrengthDao = + database.signalStrengthDao() + + @Provides + fun providesConnectivityDao(database: AppDatabase): ConnectivityDao = + database.connectivityDao() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/modules/DataModule.kt b/app/src/main/java/com/lcl/lclmeasurementtool/modules/DataModule.kt new file mode 100644 index 0000000..bf90d46 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/modules/DataModule.kt @@ -0,0 +1,61 @@ +package com.lcl.lclmeasurementtool.modules + +import com.lcl.lclmeasurementtool.datasource.ConnectivityMonitorDataSource +import com.lcl.lclmeasurementtool.datasource.LocationDataSource +import com.lcl.lclmeasurementtool.datasource.SignalStrengthDataSource +import com.lcl.lclmeasurementtool.datasource.SimStateMonitorDataSource +import com.lcl.lclmeasurementtool.location.LocationService +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel +import com.lcl.lclmeasurementtool.model.repository.* +import com.lcl.lclmeasurementtool.networking.NetworkMonitor +import com.lcl.lclmeasurementtool.networking.SimStateMonitor +import com.lcl.lclmeasurementtool.telephony.SignalStrengthMonitor +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface DataModule { + @Binds + fun bindsUserDataRepository( + userDataRepository: LocalUserDataRepository + ): UserDataRepository + + @Binds + fun bindsNetworkMonitor( + networkMonitor: ConnectivityMonitorDataSource + ): NetworkMonitor + + @Binds + fun bindsSimStateMonitor( + simStateMonitor: SimStateMonitorDataSource + ): SimStateMonitor + + @Binds + fun bindsLocationService( + locationService: LocationDataSource + ): LocationService + + @Binds + fun bindsSignalStrengthMonitor( + signalStrengthDataSource: SignalStrengthDataSource + ): SignalStrengthMonitor + + @Binds + fun bindsNetworkAPI( + lclApiRepository: LCLApiRepository + ): NetworkApiRepository + + @Binds + fun bindsSignalStrengthRepository( + measurementRepository: SignalStrengthRepository + ): HistoryDataRepository + + @Binds + fun bindsConnectivityRepository( + measurementRepository: ConnectivityRepository + ): HistoryDataRepository +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/modules/DatabaseModule.kt b/app/src/main/java/com/lcl/lclmeasurementtool/modules/DatabaseModule.kt new file mode 100644 index 0000000..e0ca248 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/modules/DatabaseModule.kt @@ -0,0 +1,25 @@ +package com.lcl.lclmeasurementtool.modules + +import android.content.Context +import androidx.room.Room +import com.lcl.lclmeasurementtool.database.db.AppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun providesLCLDatabase(@ApplicationContext context: Context): AppDatabase = + Room.databaseBuilder( + context, + AppDatabase::class.java, + "lcl-database" + ).build() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/modules/NetworkModule.kt b/app/src/main/java/com/lcl/lclmeasurementtool/modules/NetworkModule.kt new file mode 100644 index 0000000..1cdc688 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/modules/NetworkModule.kt @@ -0,0 +1,17 @@ +package com.lcl.lclmeasurementtool.modules + +import com.lcl.lclmeasurementtool.networking.RetrofitLCLNetwork +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + fun providesLCLNetworkApi(): RetrofitLCLNetwork = RetrofitLCLNetwork() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkAPI.kt b/app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkAPI.kt new file mode 100644 index 0000000..fda403c --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkAPI.kt @@ -0,0 +1,44 @@ +package com.lcl.lclmeasurementtool.networking + +import com.lcl.lclmeasurementtool.constants.NetworkConstants +import okhttp3.OkHttpClient +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.scalars.ScalarsConverterFactory +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST +import javax.inject.Singleton + +private interface NetworkAPI { + + @Headers("Content-Type: ${NetworkConstants.MEDIA_TYPE}") + @POST(value = NetworkConstants.REGISTRATION_ENDPOINT) + suspend fun register(@Body registration: String): ResponseBody + + @Headers("Content-Type: ${NetworkConstants.MEDIA_TYPE}") + @POST(value = NetworkConstants.SIGNAL_ENDPOINT) + suspend fun uploadSignalStrength(@Body signalStrengthReportModel: String): ResponseBody + + @Headers("Content-Type: ${NetworkConstants.MEDIA_TYPE}") + @POST(value = NetworkConstants.CONNECTIVITY_ENDPOINT) + suspend fun uploadConnectivity(@Body connectivityReportModel: String): ResponseBody +} + +@Singleton +class RetrofitLCLNetwork { + private val networkApi: NetworkAPI by lazy { + Retrofit.Builder() + .client(OkHttpClient()) + .addConverterFactory(ScalarsConverterFactory.create()) + .baseUrl(NetworkConstants.URL) + .build().create(NetworkAPI::class.java) + } + + + suspend fun register(registration: String) = networkApi.register(registration) + suspend fun uploadSignalStrength(signalStrengthReportModel: String) = networkApi.uploadSignalStrength(signalStrengthReportModel) + suspend fun uploadConnectivity(connectivityReportModel: String) = networkApi.uploadConnectivity(connectivityReportModel) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkMonitor.kt b/app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkMonitor.kt new file mode 100644 index 0000000..2f008df --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/networking/NetworkMonitor.kt @@ -0,0 +1,7 @@ +package com.lcl.lclmeasurementtool.networking + +import kotlinx.coroutines.flow.Flow + +interface NetworkMonitor { + val isOnline: Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/networking/SimStateMonitor.kt b/app/src/main/java/com/lcl/lclmeasurementtool/networking/SimStateMonitor.kt new file mode 100644 index 0000000..bcca6a4 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/networking/SimStateMonitor.kt @@ -0,0 +1,7 @@ +package com.lcl.lclmeasurementtool.networking + +import kotlinx.coroutines.flow.Flow + +interface SimStateMonitor { + val isSimCardInserted: Flow +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/sync/DelegatingWorker.kt b/app/src/main/java/com/lcl/lclmeasurementtool/sync/DelegatingWorker.kt new file mode 100644 index 0000000..b29ad86 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/sync/DelegatingWorker.kt @@ -0,0 +1,64 @@ +package com.lcl.lclmeasurementtool.sync + +import android.content.Context +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import kotlin.reflect.KClass + +/** + * An entry point to retrieve the [HiltWorkerFactory] at runtime + */ +@EntryPoint +@InstallIn(SingletonComponent::class) +interface HiltWorkerFactoryEntryPoint { + fun hiltWorkerFactory(): HiltWorkerFactory +} + +private const val WORKER_CLASS_NAME = "RouterWorkerDelegateClassName" + +/** + * Adds metadata to a WorkRequest to identify what [CoroutineWorker] the [DelegatingWorker] should + * delegate to + */ +internal fun KClass.delegatedData() = + Data.Builder() + .putString(WORKER_CLASS_NAME, qualifiedName) + .build() + +/** + * A worker that delegates sync to another [CoroutineWorker] constructed with a [HiltWorkerFactory]. + * + * This allows for creating and using [CoroutineWorker] instances with extended arguments + * without having to provide a custom WorkManager configuration that the app module needs to utilize. + * + * In other words, it allows for custom workers in a library module without having to own + * configuration of the WorkManager singleton. + */ +class DelegatingWorker( + appContext: Context, + workerParams: WorkerParameters, +) : CoroutineWorker(appContext, workerParams) { + + private val workerClassName = + workerParams.inputData.getString(WORKER_CLASS_NAME) ?: "" + + private val delegateWorker = + EntryPointAccessors.fromApplication(appContext) + .hiltWorkerFactory() + .createWorker(appContext, workerClassName, workerParams) + as? CoroutineWorker + ?: throw IllegalArgumentException("Unable to find appropriate worker") + + override suspend fun getForegroundInfo(): ForegroundInfo = + delegateWorker.getForegroundInfo() + + override suspend fun doWork(): Result = + delegateWorker.doWork() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/sync/SyncInitializer.kt b/app/src/main/java/com/lcl/lclmeasurementtool/sync/SyncInitializer.kt new file mode 100644 index 0000000..3c51559 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/sync/SyncInitializer.kt @@ -0,0 +1,40 @@ +package com.lcl.lclmeasurementtool.sync + +import android.content.Context +import android.util.Log +import androidx.startup.AppInitializer +import androidx.startup.Initializer +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.WorkManager +import androidx.work.WorkManagerInitializer + +object Sync { + // FROM Android Doc: + // This method is a workaround to manually initialize the sync process instead of relying on + // automatic initialization with Androidx Startup. It is called from the app module's + // Application.onCreate() and should be only done once. + fun initialize(context: Context) { + Log.d("Sync", "Sync.initialize called") + AppInitializer.getInstance(context) + .initializeComponent(SyncInitializer::class.java) + } +} + +internal const val SyncWorkName = "LCLDataReportSync" + +class SyncInitializer: Initializer { + override fun create(context: Context): Sync { + WorkManager.getInstance(context).apply { + enqueueUniquePeriodicWork( + SyncWorkName, + ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, + UploadWorker.periodicSyncWork() + ) + } + Log.d("SyncInitializer", "SyncInitializer initialize here!") + return Sync + } + + override fun dependencies(): List>> = + listOf(WorkManagerInitializer::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/sync/UploadWorker.kt b/app/src/main/java/com/lcl/lclmeasurementtool/sync/UploadWorker.kt new file mode 100644 index 0000000..476b8e4 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/sync/UploadWorker.kt @@ -0,0 +1,70 @@ +package com.lcl.lclmeasurementtool.sync + +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.* +import com.lcl.lclmeasurementtool.datastore.Dispatcher +import com.lcl.lclmeasurementtool.datastore.LCLDispatchers +import com.lcl.lclmeasurementtool.model.repository.ConnectivityRepository +import com.lcl.lclmeasurementtool.model.repository.SignalStrengthRepository +import com.lcl.lclmeasurementtool.util.Synchronizer +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import java.time.Duration +import java.util.concurrent.TimeUnit + +@HiltWorker +class UploadWorker @AssistedInject constructor( + @Assisted private val context: Context, + @Assisted workerParameters: WorkerParameters, + private val signalStrengthRepository: SignalStrengthRepository, + private val connectivityRepository: ConnectivityRepository, + @Dispatcher(LCLDispatchers.IO) private val ioDispatcher: CoroutineDispatcher +): CoroutineWorker(context, workerParameters), Synchronizer { + + override suspend fun doWork(): Result = + withContext(ioDispatcher) { + try { + val syncSuccessfully = awaitAll( + async { + val b = signalStrengthRepository.sync() + Log.d(TAG, "signal strength repository finish sync") + b + }, + async { + val b = connectivityRepository.sync() + Log.d(TAG, "connectivity repository finish sync") + b + } + ).all { it } + if (syncSuccessfully) { + Log.d(TAG, "upload successfully") + return@withContext Result.success() + } else { + Log.d(TAG, "upload failed. some sync work failed") + return@withContext Result.failure() + } + } catch (e: Exception) { + Log.d(TAG, "upload failed. exception is $e") + Result.failure() + } + } + + companion object { + fun periodicSyncWork() = + PeriodicWorkRequestBuilder(4, TimeUnit.HOURS) + .setConstraints(Constraints(requiredNetworkType = NetworkType.CONNECTED)) + .setInitialDelay(5, TimeUnit.MINUTES) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, Duration.ofMinutes(10)) + .setInputData(UploadWorker::class.delegatedData()) + .build() + + const val TAG = "UploadWorker" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthLevel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthLevel.kt new file mode 100644 index 0000000..badc36f --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthLevel.kt @@ -0,0 +1,49 @@ +package com.lcl.lclmeasurementtool.telephony + +import androidx.compose.ui.graphics.Color + + +enum class SignalStrengthLevelEnum(val level: Int) { + + NONE(0) { + override fun color(): Color { + return Color.LightGray + } + }, + + // the signal strength is poor or unknown + POOR(1) { + override fun color(): Color { + return Color.Red + } + }, + + // the signal strength is weak + MODERATE(2) { + override fun color(): Color { + return Color(0xFFF47E4C) // orange + } + }, + + // the signal strength is moderate + GOOD(3) { + override fun color(): Color { + return Color(0xFF81FF50) // light green + } + }, + + // the signal strength is good + GREAT(4) { + override fun color(): Color { + return Color.Green + } + }; + + abstract fun color(): Color + + companion object { + fun init(value: Int): SignalStrengthLevelEnum { + return SignalStrengthLevelEnum.values().first { it.level == value } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthMonitor.kt b/app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthMonitor.kt new file mode 100644 index 0000000..8a814a9 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/telephony/SignalStrengthMonitor.kt @@ -0,0 +1,9 @@ +package com.lcl.lclmeasurementtool.telephony + +import android.telephony.SignalStrength +import kotlinx.coroutines.flow.Flow + +interface SignalStrengthMonitor { + val signalStrength: Flow + fun getCellID(): String +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/AlertDialog.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/AlertDialog.kt new file mode 100644 index 0000000..15f396a --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/AlertDialog.kt @@ -0,0 +1,43 @@ +package com.lcl.lclmeasurementtool.ui + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.vector.ImageVector + +@Composable +fun Dialog( + icon: ImageVector, + onConfirmClicked: () -> Unit, + title: String, + text: String, + +) { + val openDialog = remember { mutableStateOf(true) } + if (openDialog.value) { + AlertDialog( + icon = { Icon(imageVector = icon, contentDescription = null)}, + onDismissRequest = { }, + title = { + Text(text = title) + }, + text = { + Text(text) + }, + confirmButton = { + TextButton( + onClick = { + onConfirmClicked() + openDialog.value = false + } + ) { + Text("Confirm") + } + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/AppState.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/AppState.kt new file mode 100644 index 0000000..7b96a51 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/AppState.kt @@ -0,0 +1,98 @@ +package com.lcl.lclmeasurementtool.ui + +import android.util.Log +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.* +import androidx.navigation.NavDestination +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import com.lcl.lclmeasurementtool.networking.NetworkMonitor +import com.lcl.lclmeasurementtool.networking.SimStateMonitor +import com.lcl.lclmeasurementtool.ui.navigation.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + + +@Composable +fun rememberAppState( + windowSizeClass: WindowSizeClass, + networkMonitor: NetworkMonitor, + simStateMonitor: SimStateMonitor, + coroutineScope: CoroutineScope = rememberCoroutineScope(), + navController: NavHostController = rememberNavController() +) : AppState { + return remember(navController, coroutineScope, windowSizeClass, networkMonitor, simStateMonitor) { + AppState(navController, coroutineScope, windowSizeClass, networkMonitor, simStateMonitor) + } +} + +@Stable +class AppState( + val navController: NavHostController, + val coroutineScope: CoroutineScope, + val windowSizeClass: WindowSizeClass, + val networkMonitor: NetworkMonitor, + simStateMonitor: SimStateMonitor +) { + + var shouldShowSettingsDialog by mutableStateOf(false) + private set + + + val currentTopLevelDestination : TopLevelDestination? + @Composable get() = when(currentDestination?.route) { + homeNavigationRoute -> TopLevelDestination.HOME + historyRoute -> TopLevelDestination.HISTORY + else -> null + } + + val isOffline = networkMonitor.isOnline + .map(Boolean::not) + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = false + ) + + + val isSimCardInserted = simStateMonitor.isSimCardInserted + .stateIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = true + ) + + fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) { + Log.d("AppState","navigate to $topLevelDestination") + val topLevelNavOption = navOptions { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + + when(topLevelDestination) { + TopLevelDestination.HOME -> navController.navigateToHome(topLevelNavOption) + TopLevelDestination.HISTORY -> navController.navigateToHistory(topLevelNavOption) + } + } + + fun onBackClick() { + navController.popBackStack() + } + + val currentDestination: NavDestination? + @Composable get() = navController.currentBackStackEntryAsState().value?.destination + + val topLevelDestinations: List = TopLevelDestination.values().asList() + + fun setShowSettingsDialog(shouldShow: Boolean) { + shouldShowSettingsDialog = shouldShow + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/ConnectivityItem.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/ConnectivityItem.kt new file mode 100644 index 0000000..6c2be91 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/ConnectivityItem.kt @@ -0,0 +1,105 @@ +package com.lcl.lclmeasurementtool.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.lcl.lclmeasurementtool.model.datamodel.ConnectivityReportModel + +@Composable +fun ConnectivityItem( + data: ConnectivityReportModel, + modifier: Modifier = Modifier, + onClick: () -> Unit, + itemSeparation: Dp = 12.dp +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(1f) + .padding(vertical = itemSeparation) + .padding(start = 10.dp) + ) { + TagIcon(modifier = modifier, icon = Rounded.NetworkCheck) + Spacer(modifier = Modifier.width(12.dp)) + ConnectivityContent(data = data) + } + + if (data.reported) { + TagLabel("Reported", modifier = Modifier.padding(end = 4.dp)) + } + } +} + +@Composable +private fun ConnectivityContent(data: ConnectivityReportModel, modifier: Modifier = Modifier) { + Column(modifier) { + Row { + Column { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Text(text = "Speed") + DataEntry(icon = Rounded.CloudUpload, text = "${data.uploadSpeed} mbps") + DataEntry(icon = Rounded.CloudDownload, text = "${data.downloadSpeed} mbps") + } + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Text(text = "Ping") + DataEntry(icon = Rounded.NetworkPing, text = "${data.ping} ms") + DataEntry(icon = Rounded.Cancel, text = "${data.packetLoss}%") + } + } + } + Text( + text = data.timestamp, + style = MaterialTheme.typography.bodySmall + ) + } +} + + +@Preview +@Composable +private fun InterestsCardPreview() { + val data = ConnectivityReportModel(123.123, 345.345, "timestamp2", "hi2", "deviceID2", 123.32, 345.52, 23.22, 10.3, reported = true) + Surface { + ConnectivityItem( + data = data, + onClick = { } + ) + } +} + +//@Preview +//@Composable +//private fun InterestsCardLongNamePreview() { +// val data = ConnectivityReportModel(123.123, 345.345, "timestamp2", "hi2", "deviceID2", 123.32, 345.52, 23.22, 10.3) +// Surface { +// ConnectivityItem( +// data = data, +// onClick = { } +// ) +// } +//} +// +//@Preview +//@Composable +//private fun InterestsCardLongDescriptionPreview() { +// val data = ConnectivityReportModel(123.123, 345.345, "timestamp2", "hi2", "deviceID2", 123.32, 345.52, 23.22, 10.3) +// Surface { +// ConnectivityItem( +// data = data, +// onClick = { } +// ) +// } +//} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryItem.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryItem.kt new file mode 100644 index 0000000..8918f21 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryItem.kt @@ -0,0 +1,6 @@ +package com.lcl.lclmeasurementtool.ui + +enum class HistoryItem { + SIGNAL, + CONNECTIVITY +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryScreen.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryScreen.kt new file mode 100644 index 0000000..9e3f60c --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HistoryScreen.kt @@ -0,0 +1,153 @@ +package com.lcl.lclmeasurementtool.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.* +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.lcl.lclmeasurementtool.model.viewmodels.ConnectivityUiState +import com.lcl.lclmeasurementtool.model.viewmodels.ConnectivityViewModel +import com.lcl.lclmeasurementtool.model.viewmodels.SignalStrengthUiState +import com.lcl.lclmeasurementtool.model.viewmodels.SignalStrengthViewModel + +@Composable +fun HistoryRoute( + signalStrengthViewModel: SignalStrengthViewModel = hiltViewModel(), + connectivityViewModel: ConnectivityViewModel = hiltViewModel() +) { + HistoryScreen(signalStrengthViewModel, connectivityViewModel) +} + +@Composable +fun HistoryScreen( + signalStrengthViewModel: SignalStrengthViewModel, + connectivityViewModel: ConnectivityViewModel +) { + var selectedTabIndex by remember { mutableStateOf(0) } + Column { + LCLTabRow(selectedTabIndex = selectedTabIndex) { + HistoryItem.values().forEachIndexed { index, item -> + LCLTab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + text = {Text(text = item.name)} + ) + } + } + LCLTabContents(selectedTabIndex = selectedTabIndex, signalStrengthViewModel, connectivityViewModel) + } +} + +@Composable +fun LCLTabContents( + selectedTabIndex: Int, + signalStrengthViewModel: SignalStrengthViewModel, + connectivityViewModel: ConnectivityViewModel +) { + val signalUiState: SignalStrengthUiState by signalStrengthViewModel.dataFlow.collectAsStateWithLifecycle() + val connectivityUiState: ConnectivityUiState by connectivityViewModel.dataFlow.collectAsStateWithLifecycle() + LazyColumn { + when(selectedTabIndex) { + 0 -> { + item {SignalStrengthItems(signalUiState)} + } + 1 -> { + item { ConnectivityItems(connectivityUiState) } + } + } + } +} + +@Composable +private fun SignalStrengthItems(uiState: SignalStrengthUiState) { + when(uiState) { + SignalStrengthUiState.Loading -> LCLLoadingWheel(contentDesc = "Loading data ...") + is SignalStrengthUiState.Success -> { + uiState.signalStrengths.forEach { + SignalStrengthItem(data = it, onClick = { }) + } + } + is SignalStrengthUiState.Error -> ErrorScreen() + } +} + +@Composable +private fun ConnectivityItems(uiState: ConnectivityUiState) { + when(uiState) { + ConnectivityUiState.Loading -> LCLLoadingWheel(contentDesc = "Loading data ...") + is ConnectivityUiState.Success -> { + uiState.connectivities.forEach { + ConnectivityItem(data = it, onClick = { }) + } + } + is ConnectivityUiState.Error -> ErrorScreen() + } +} + +@Composable +private fun ErrorScreen() { + Text(text = "Error in loading data") +} + +@Composable +fun LCLTab( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + text: @Composable () -> Unit +) { + Tab( + selected = selected, + onClick = onClick, + modifier = modifier, + enabled = enabled, + text = { + val style = MaterialTheme.typography.labelLarge.copy(textAlign = TextAlign.Center) + ProvideTextStyle(value = style) { + Box(modifier = Modifier.padding(top = 7.dp)) { + text() + } + } + } + ) +} + +@Composable +fun LCLTabRow( + selectedTabIndex: Int, + modifier: Modifier = Modifier, + tabs: @Composable () -> Unit +) { + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = modifier, + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + indicator = { + TabRowDefaults.Indicator( + modifier = Modifier.tabIndicatorOffset(it[selectedTabIndex]), + height = 2.dp, + color = MaterialTheme.colorScheme.onSurface + + ) + }, + tabs = tabs + ) +} + +//@Preview +//@Composable +//fun HistoryPreview() { +// BoxWithConstraints { +// HistoryScreen() +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt new file mode 100644 index 0000000..5e616ad --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/HomeScreen.kt @@ -0,0 +1,256 @@ +package com.lcl.lclmeasurementtool.ui + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons.Filled +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.filled.NetworkCheck +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.SignalCellularAlt +import androidx.compose.material.icons.rounded.Cancel +import androidx.compose.material.icons.rounded.CloudDownload +import androidx.compose.material.icons.rounded.CloudUpload +import androidx.compose.material.icons.rounded.NetworkPing +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.lcl.lclmeasurementtool.BuildConfig +import com.lcl.lclmeasurementtool.ConnectivityTestResult +import com.lcl.lclmeasurementtool.MainActivityViewModel +import com.lcl.lclmeasurementtool.PingResultState +import com.lcl.lclmeasurementtool.SignalStrengthResult +import com.lcl.lclmeasurementtool.features.ping.PingError +import com.lcl.lclmeasurementtool.features.ping.PingErrorCase +import kotlinx.coroutines.cancel + +@Composable +fun HomeRoute(isOffline: Boolean, mainActivityViewModel: MainActivityViewModel) { + HomeScreen(isOffline = isOffline, mainActivityViewModel = mainActivityViewModel) +} + +@Composable +fun HomeScreen(modifier: Modifier = Modifier, isOffline: Boolean, mainActivityViewModel: MainActivityViewModel) { + + val offline by remember { mutableStateOf(isOffline) } + val snackbarHostState = remember { SnackbarHostState() } + + val isMLabTestActive = mainActivityViewModel.isMLabTestActive.collectAsStateWithLifecycle() + val mlabPingResult = mainActivityViewModel.mLabPingResult.collectAsStateWithLifecycle() + val mlabUploadResult = mainActivityViewModel.mlabUploadResult.collectAsStateWithLifecycle() + val mlabDownloadResult = mainActivityViewModel.mlabDownloadResult.collectAsStateWithLifecycle() + val signalStrength = mainActivityViewModel.signalStrengthResult.collectAsStateWithLifecycle() + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = modifier.fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SignalStrengthCard(modifier = modifier, signalStrengthResult = signalStrength.value) + ConnectivityCard( + label = "MLab", + modifier = modifier, + pingResult = mlabPingResult.value, + uploadResult = mlabUploadResult.value, + downloadResult = mlabDownloadResult.value + ) + + if (isMLabTestActive.value) { + LCLLoadingWheel(contentDesc = "") + } + } + + FloatingActionButton(onClick = { + + if (BuildConfig.FLAVOR == "full" && offline) { + Log.d("HomeScreen", "device is currently offline") + return@FloatingActionButton + } + + if (!isMLabTestActive.value) { + mainActivityViewModel.runMLabTest() + } else { + coroutineScope.cancel("User initiated the cancellation (mlab)") + mainActivityViewModel.cancelMLabTest() + } + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 12.dp, bottom = 12.dp)) { + Icon(imageVector = if (isMLabTestActive.value) Filled.Pause else Filled.PlayArrow, contentDescription = null) + } + } + + if (BuildConfig.FLAVOR == "full" && offline) { + ShowMessage(isOffline = true, msg = "Your Device is offline. Please connect to the Internet via Cellular network", snackbarHostState = snackbarHostState) + } +} + +@Composable +fun ShowMessage(isOffline: Boolean, msg: String, snackbarHostState: SnackbarHostState) { + LaunchedEffect(isOffline) { + snackbarHostState.showSnackbar(message = msg, duration = SnackbarDuration.Long) + } +} + +@Composable +private fun SignalStrengthCard( + modifier: Modifier = Modifier, + signalStrengthResult: SignalStrengthResult +) { + val fontSize = 18.sp + + val (dbm, level) = signalStrengthResult + + Card(colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceVariant), + modifier = Modifier + .padding(horizontal = 10.dp, vertical = 10.dp) + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(20.dp) + ) { + + Icon(modifier = modifier, + imageVector = Filled.SignalCellularAlt, + contentDescription = null) + Spacer(modifier = Modifier.width(12.dp)) + Text(text = "Signal Strength:", fontSize = fontSize) + Spacer(modifier = Modifier.width(4.dp)) + Text(text = "$dbm", fontWeight = FontWeight.Bold, fontSize = fontSize) + Spacer(modifier = Modifier.width(4.dp)) + Text(text = "dBm", fontSize = fontSize) + Spacer(modifier = Modifier.width(20.dp)) + Box(modifier = Modifier + .size(10.dp) + .clip( + CircleShape + ) + .background(level.color())) + } + } +} + +@Composable +private fun ConnectivityCard( + label: String, + modifier: Modifier = Modifier, + pingResult: PingResultState, + uploadResult: ConnectivityTestResult, + downloadResult: ConnectivityTestResult, +) { + Card(colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceVariant), + modifier = Modifier + .padding(horizontal = 10.dp, vertical = 10.dp) + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding(start = 12.dp, end = 12.dp, top = 12.dp) + ) { + + Icon(modifier = modifier.padding(end = 12.dp), + imageVector = Filled.NetworkCheck, + contentDescription = null) + Column { + Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) { + when (uploadResult) { + is ConnectivityTestResult.Result -> { + DataEntry(icon = Rounded.CloudUpload, text = "${uploadResult.result} Mbps") + } + else -> DataEntry(icon = Rounded.CloudUpload, text = "0.0 Mbps") + } + + + when (downloadResult) { + is ConnectivityTestResult.Result -> { + DataEntry(icon = Rounded.CloudDownload, text = "${downloadResult.result} Mbps") + } + else -> { + DataEntry(icon = Rounded.CloudDownload, text = "0.0 Mbps") + } + } + + } + Row(horizontalArrangement = Arrangement.spacedBy(20.dp)) { + var pingNum = "0" + var pingLoss = "0" + when(pingResult) { + is PingResultState.Success -> { + pingNum = pingResult.result.avg!! + pingLoss = pingResult.result.numLoss!! + } + is PingResultState.Error -> { + pingNum = "0" + pingLoss = "0" + if (pingResult.error.code != PingErrorCase.OK) { + Log.d("HOMEScreen", pingResult.error.message ?: "Error occurred") + // TODO: show error message + } + } + } + + DataEntry(icon = Rounded.NetworkPing, text = "$pingNum ms") + DataEntry(icon = Rounded.Cancel, text = "$pingLoss % loss") + } + } + } + Box(modifier = Modifier.fillMaxWidth()) { + Text(text = "Powered by $label", fontWeight = FontWeight.Thin, fontSize = 10.sp, modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 10.dp, bottom = 4.dp)) + } + } +} + +@Composable +fun DataEntry(icon: ImageVector, text: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(imageVector = icon, contentDescription = null) + Spacer(modifier = Modifier.width(4.dp)) + Text(text = text, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) + } +} + +@Preview +@Composable +fun ConnectivityCardPreview() { + + Column { + ConnectivityCard(label = "IperfRunner", + pingResult = PingResultState.Error(PingError(PingErrorCase.OK, null)), + uploadResult = ConnectivityTestResult.Result("1", Color.Blue), + downloadResult = ConnectivityTestResult.Result("1", Color.Green) + ) + + ConnectivityCard(label = "MLab", + pingResult = PingResultState.Error(PingError(PingErrorCase.OK, null)), + uploadResult = ConnectivityTestResult.Result("1", Color.Blue), + downloadResult = ConnectivityTestResult.Result("1", Color.Green) + ) + } +} + + +//@Preview +//@Composable +//fun HomePreview() { +// BoxWithConstraints { +// HomeScreen(isOffline = false) +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLApplication.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLApplication.kt new file mode 100644 index 0000000..0e28a60 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLApplication.kt @@ -0,0 +1,203 @@ +package com.lcl.lclmeasurementtool.ui + +import android.util.Log +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import com.lcl.lclmeasurementtool.BuildConfig +import com.lcl.lclmeasurementtool.MainActivityViewModel + +import com.lcl.lclmeasurementtool.networking.NetworkMonitor +import com.lcl.lclmeasurementtool.networking.SimStateMonitor +import com.lcl.lclmeasurementtool.ui.navigation.TopLevelDestination +import com.lcl.lclmeasurementtool.ui.navigation.historyGraph +import com.lcl.lclmeasurementtool.ui.navigation.homeNavigationRoute +import com.lcl.lclmeasurementtool.ui.navigation.homeScreen + +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) +@Composable +fun LCLApp( + windowSizeClass: WindowSizeClass, + networkMonitor: NetworkMonitor, + simStateMonitor: SimStateMonitor, + mainViewModel: MainActivityViewModel = hiltViewModel(), +// shouldLogin: Boolean, + appState: AppState = rememberAppState(windowSizeClass = windowSizeClass, networkMonitor = networkMonitor, simStateMonitor = simStateMonitor) +) { +// Log.d("LCLAPP", "isLoggedIn=$shouldLogin") +// if (shouldLogin) { +// +// return +// } + + + if (appState.shouldShowSettingsDialog) { + SettingsDialog( + onDismiss = { appState.setShowSettingsDialog(false) } + ) + } + + val snackbarHostState = remember { SnackbarHostState() } + val isOffline by appState.isOffline.collectAsStateWithLifecycle() + val isSimCardInserted by appState.isSimCardInserted.collectAsStateWithLifecycle() + + Log.d("LCLApplication", "isOffline is $isOffline") + Log.d("LCLApplication", "isSimCardInserted is $isSimCardInserted") + if (BuildConfig.FLAVOR.equals("full")) { + LaunchedEffect(isOffline) { + if (isOffline) { + Log.d("LCLApplication", "show snack bar") + snackbarHostState.showSnackbar(message = "Please connect to a cellular network before running the test", duration = SnackbarDuration.Indefinite) + } + } + + if (!isSimCardInserted) { + Dialog(icon = LCLIcons.NoSIM, + onConfirmClicked = { + // should logout + mainViewModel.logout() + Log.d("LCLApplication", "confirm!") }, + title = "Error", + text = "Please insert the sim card") + } + } + + + Scaffold( + containerColor = Color.Transparent, + contentWindowInsets = WindowInsets(0,0,0,0), + contentColor = MaterialTheme.colorScheme.onBackground, + bottomBar = { + LCLBottomBar(destinations = appState.topLevelDestinations, onNavigateToDestination = appState::navigateToTopLevelDestination, currentDestination = appState.currentDestination) + }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } + ) { paddingValues -> + + Row(modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .consumedWindowInsets( + WindowInsets.safeDrawing.only( + WindowInsetsSides.Horizontal + ) + )) { + Column(modifier = Modifier.fillMaxSize()) { + val destination = appState.currentTopLevelDestination + if (destination != null) { + AppTopBar( + titleRes = destination.titleTextId, + actionIcon = LCLIcons.Settings, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + onActionClick = { appState.setShowSettingsDialog(true) } + ) + } + LCLNavHost(navController = appState.navController, onBackClick = appState::onBackClick, isOffline = isOffline, mainViewModel = mainViewModel) + } + } + + } +} + +@Composable +fun LCLBottomBar( + destinations: List, + onNavigateToDestination: (TopLevelDestination) -> Unit, + currentDestination: NavDestination?, + modifier: Modifier = Modifier +) { + + NavigationBar( + modifier = Modifier, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + tonalElevation = 0.dp) { + + destinations.forEach { destination -> + val selected = currentDestination.isTopLevelDestinationInHierarchy(destination) + + NavigationBarItem( + selected = selected, + onClick = {onNavigateToDestination(destination)}, + icon = { + val icon = if(selected) { + destination.selectedIcon + } else { + destination.unselectedIcon + } + + Icon( + imageVector = (icon as Icon.ImageVectorIcon).imageVector, + contentDescription = null + ) + }, + label = { Text(stringResource(id = destination.iconTextId)) }, + alwaysShowLabel = true, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + selectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + indicatorColor = MaterialTheme.colorScheme.primaryContainer + ) + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppTopBar(@StringRes titleRes: Int, + actionIcon: ImageVector, + modifier: Modifier = Modifier, + colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), + onActionClick: () -> Unit = {} +) { + CenterAlignedTopAppBar( + title = { Text(text = stringResource(id = titleRes)) }, + colors = colors, + modifier = modifier, + actions = { + IconButton(onClick = onActionClick) { + Icon( + imageVector = actionIcon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + ) +} + +@Composable +fun LCLNavHost( + navController: NavHostController, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + startDestination: String = homeNavigationRoute, + isOffline: Boolean, + mainViewModel: MainActivityViewModel +) { + NavHost(navController = navController, modifier = modifier, startDestination = startDestination) { + homeScreen(isOffline, mainViewModel) + historyGraph() + } +} + +private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) = + this?.hierarchy?.any { + it.route?.contains(destination.name, true) ?: false + } ?: false \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLIcons.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLIcons.kt new file mode 100644 index 0000000..e0d414f --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLIcons.kt @@ -0,0 +1,25 @@ +package com.lcl.lclmeasurementtool.ui + +import androidx.annotation.DrawableRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.NoSim +import androidx.compose.material.icons.filled.TableChart +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.TableChart +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.ui.graphics.vector.ImageVector + +object LCLIcons { + val Home = Icons.Filled.Home + val HomeBorder = Icons.Outlined.Home + val HistoryData = Icons.Filled.TableChart + val HistoryDataBorder = Icons.Outlined.TableChart + val Settings = Icons.Rounded.Settings + val NoSIM = Icons.Filled.NoSim +} + +sealed class Icon { + data class ImageVectorIcon(val imageVector: ImageVector) : Icon() + data class DrawableResourceIcon(@DrawableRes val id: Int) : Icon() +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLLloadingWheel.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLLloadingWheel.kt new file mode 100644 index 0000000..9cf3a0e --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/LCLLloadingWheel.kt @@ -0,0 +1,106 @@ +package com.lcl.lclmeasurementtool.ui + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + + +private const val ROTATION_TIME = 12000 +private const val NUM_OF_LINES = 12 + + +// from Now in Android repo: https://github.com/android/nowinandroid + +@Composable +fun LCLLoadingWheel( + contentDesc: String, + modifier: Modifier = Modifier +) { + val infiniteTransition = rememberInfiniteTransition() + + // Specifies the float animation for slowly drawing out the lines on entering + val startValue = if (LocalInspectionMode.current) 0F else 1F + val floatAnimValues = (0 until NUM_OF_LINES).map { remember { Animatable(startValue) } } + LaunchedEffect(floatAnimValues) { + (0 until NUM_OF_LINES).map { index -> + launch { + floatAnimValues[index].animateTo( + targetValue = 0F, + animationSpec = tween( + durationMillis = 100, + easing = FastOutSlowInEasing, + delayMillis = 40 * index + ) + ) + } + } + } + + // Specifies the rotation animation of the entire Canvas composable + val rotationAnim by infiniteTransition.animateFloat( + initialValue = 0F, + targetValue = 360F, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = ROTATION_TIME, easing = LinearEasing) + ) + ) + + // Specifies the color animation for the base-to-progress line color change + val baseLineColor = MaterialTheme.colorScheme.onBackground + val progressLineColor = MaterialTheme.colorScheme.inversePrimary + val colorAnimValues = (0 until NUM_OF_LINES).map { index -> + infiniteTransition.animateColor( + initialValue = baseLineColor, + targetValue = baseLineColor, + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = ROTATION_TIME / 2 + progressLineColor at ROTATION_TIME / NUM_OF_LINES / 2 with LinearEasing + baseLineColor at ROTATION_TIME / NUM_OF_LINES with LinearEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(ROTATION_TIME / NUM_OF_LINES / 2 * index) + ) + ) + } + + // Draws out the LoadingWheel Canvas composable and sets the animations + Canvas( + modifier = modifier + .size(48.dp) + .padding(8.dp) + .graphicsLayer { rotationZ = rotationAnim } + .semantics { contentDescription = contentDesc } + ) { + repeat(NUM_OF_LINES) { index -> + rotate(degrees = index * 30f) { + drawLine( + color = colorAnimValues[index].value, + // Animates the initially drawn 1 pixel alpha from 0 to 1 + alpha = if (floatAnimValues[index].value < 1f) 1f else 0f, + strokeWidth = 4F, + cap = StrokeCap.Round, + start = Offset(size.width / 2, size.height / 4), + end = Offset(size.width / 2, floatAnimValues[index].value * size.height / 4) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/Login.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/Login.kt new file mode 100644 index 0000000..22c6498 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/Login.kt @@ -0,0 +1,92 @@ +package com.lcl.lclmeasurementtool.ui + +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.lcl.lclmeasurementtool.BuildConfig +import com.lcl.lclmeasurementtool.LoginStatus +import com.lcl.lclmeasurementtool.MainActivityViewModel +import com.lcl.lclmeasurementtool.R +import io.github.g00fy2.quickie.QRResult +import io.github.g00fy2.quickie.ScanQRCode +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Login( + viewModel: MainActivityViewModel +) { + val coroutineScope = rememberCoroutineScope() + +// val context = LocalContext.current +// TipDialog.init(context) + val snackbarHostState = remember { SnackbarHostState() } + val loginStatus = viewModel.loginState.collectAsStateWithLifecycle() + val qrCodeLauncher = rememberLauncherForActivityResult(ScanQRCode()) {qrResult -> + when(qrResult) { + is QRResult.QRSuccess -> { + coroutineScope.launch { + viewModel.login(qrResult.content.rawValue) + } + } + else -> { + Log.d("MainActivityVM", "Login Failed QRCode Scan") + } + } + } + + LaunchedEffect(loginStatus.value) { + when(val status = loginStatus.value) { + is LoginStatus.Initial -> {} + + is LoginStatus.RegistrationFailed -> { + Log.d("Login", "⚠️ ${status.reason}") + snackbarHostState.showSnackbar("⚠️ ${status.reason}", duration = SnackbarDuration.Long) + } + + is LoginStatus.RegistrationSucceeded -> { + Log.d("Login", "⚠️ yeaaaa!") + } + } + } + + Scaffold( + snackbarHost = {SnackbarHost(hostState = snackbarHostState)} + ) { padding -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) { + Image(painter = painterResource(id = R.drawable.lcl_purple_gold_uw), contentDescription = null, modifier = Modifier + .width(300.dp) + .height(300.dp) + .padding(top = 50.dp)) + Button(onClick = { + if (BuildConfig.FLAVOR == "full") { + qrCodeLauncher.launch(null) + } else if (BuildConfig.FLAVOR == "demo") { + viewModel.demoLogin() + } + }, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.onPrimaryContainer), + modifier = Modifier.padding(top = 30.dp)) + { + Text(text = "Scan to Login") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt new file mode 100644 index 0000000..0b31e92 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SettingsView.kt @@ -0,0 +1,230 @@ +package com.lcl.lclmeasurementtool.ui + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.Email +import androidx.compose.material.icons.rounded.Logout +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.lcl.lclmeasurementtool.BuildConfig +import com.lcl.lclmeasurementtool.R +import com.lcl.lclmeasurementtool.model.viewmodels.SettingsViewModel + +@Composable +fun SettingsDialog( + onDismiss: () -> Unit, + viewModel: SettingsViewModel = hiltViewModel() +) { + + val showData = viewModel.shouldShowData.collectAsStateWithLifecycle() + SettingDialog( + onDismiss = onDismiss, + toggleShowData = viewModel::toggleShowData, + logout = viewModel::logout, + showData = showData.value + ) +} + +@Composable +fun SettingDialog( + onDismiss: () -> Unit, + toggleShowData: (Boolean) -> Unit, + logout: () -> Unit, + showData: Boolean +) { + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { + Text( + text = stringResource(id = R.string.settings_title), + style = MaterialTheme.typography.titleLarge + ) + }, + text = { + Divider() + Column(Modifier.verticalScroll(rememberScrollState())) { + SettingsPanel(onSelectShowData = {toggleShowData(!showData)}, onLogoutClicked = {logout()}, showData = showData) + Divider(Modifier.padding(top = 8.dp)) + LinksPanel() + VersionInfo() + } + }, + confirmButton = { + Text( + text = stringResource(R.string.dismiss_dialog_button_text), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(horizontal = 8.dp) + .clickable { onDismiss() } + ) + } + ) +} + +@Composable +private fun SettingsPanel( + onSelectShowData: (Boolean) -> Unit, + onLogoutClicked: () -> Unit, + showData: Boolean +) { + SettingsDialogSectionTitle(text = "General") + Column(Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row { + Checkbox(checked = showData, onCheckedChange = onSelectShowData) + Column { + Text(text = "Show Data on SCN website") + TextSummary(text = "Display signal and speed test data on SCN public map. Your data will help others understand our coverage!") + } + } + } + SettingsDialogSectionTitle(text = "Data Management") + Column(Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally) { + ClickableRow(text = "Export Signal Strength Data", icon = Icons.Rounded.Download, onClick = {}) + ClickableRow(text = "Export Speed Test Data", icon = Icons.Rounded.Download, onClick = {}) + } + SettingsDialogSectionTitle(text = "Help") + Column(Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally) { + ClickableRow(text = "Send Feedback", icon = Icons.Rounded.Email, onClick = {}) + ClickableRow(text = "Logout", icon = Icons.Rounded.Logout, onClick = onLogoutClicked) + } +} + +@Composable +private fun SettingsDialogSectionTitle(text: String) { + Text( + text = text, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) +} + +@Composable +private fun ClickableRow(text: String, icon: ImageVector, onClick: () -> Unit) { + Row (Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically) { + Icon(icon, contentDescription = null) + Spacer(Modifier.width(8.dp)) + Column(horizontalAlignment = Alignment.Start) { + TextButton(onClick = onClick) { + Text(text = text) + } + } + } +} + +@Composable +private fun TextSummary(text: String) { + Text(text = text, + style = TextStyle( + color = Color.Gray, + fontSize = 12.sp,) + ) +} + +@Composable +private fun VersionInfo() { + Column( + Modifier + .fillMaxWidth() + .padding(top = 10.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR})") + TextSummary(text = "By Local Connectivity Lab @ UWCSE") + } +} + +@Composable +private fun LinksPanel() { + Row( + modifier = Modifier.padding(top = 16.dp) + ) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row { + TextLink( + text = stringResource(R.string.privacy_policy), + url = PRIVACY_POLICY_URL + ) + Spacer(Modifier.width(16.dp)) + TextLink( + text = "Terms of Use", + url = TOU + ) + } + Spacer(Modifier.height(8.dp)) + Row { + TextLink(text = "Feature List", url = FEATURE_LIST) + } + } + } +} + +@Composable +private fun TextLink(text: String, url: String) { + val launchResourceIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + val context = LocalContext.current + + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clickable { + ContextCompat.startActivity(context, launchResourceIntent, null) + } + ) +} + +@Preview +@Composable +private fun PreviewSettingsDialog() { + SettingDialog( + onDismiss = {}, + toggleShowData = {}, + logout = {}, + showData = false + ) +} +// +//@Preview +//@Composable +//private fun PreviewSettingsDialogLoading() { +// NiaTheme { +// SettingsDialog( +// onDismiss = {}, +// settingsUiState = Loading, +// onChangeThemeBrand = { }, +// onChangeDarkThemeConfig = { } +// ) +// } +//} + +/* ktlint-disable max-line-length */ +private const val PRIVACY_POLICY_URL = "https://seattlecommunitynetwork.org/" +private const val TOU = "https://seattlecommunitynetwork.org/" +private const val FEATURE_LIST = "https://docs.google.com/document/d/e/2PACX-1vTWyg_FFPtKEsNIuD85VauCPy42fLQNk0QoZ6c8NzTH8-d5_AsUX4d-RDAuOoWKPMj_fHppi8Ve8VVv/pub" \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/SignalStrengthItem.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SignalStrengthItem.kt new file mode 100644 index 0000000..1eaa609 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/SignalStrengthItem.kt @@ -0,0 +1,109 @@ +package com.lcl.lclmeasurementtool.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.SignalCellularAlt +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.lcl.lclmeasurementtool.model.datamodel.SignalStrengthReportModel + +@Composable +fun SignalStrengthItem( + data: SignalStrengthReportModel, + onClick: () -> Unit, + modifier: Modifier = Modifier, + itemSeparation: Dp = 12.dp +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(1f) + .padding(vertical = itemSeparation) + .padding(start = 10.dp) + ) { + TagIcon(modifier, Icons.Rounded.SignalCellularAlt) + Spacer(modifier = Modifier.width(12.dp)) + SignalStrengthContent(data = data) + } + if (data.reported) { + TagLabel("Reported", modifier = Modifier.padding(end = 10.dp)) + } + } +} + + +@Composable +private fun SignalStrengthContent(data: SignalStrengthReportModel, modifier: Modifier = Modifier) { + Column(modifier) { + Row { + Text( + text = "${data.dbm} dBm", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding( + vertical = 4.dp + ), + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.padding(start = 10.dp)) + Text(text = "(level code: ${data.levelCode})", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding( + vertical = 4.dp + )) + } + Text( + text = data.timestamp, + style = MaterialTheme.typography.bodySmall + ) + } +} + +@Preview +@Composable +private fun InterestsCardPreview() { + val data = SignalStrengthReportModel(1.1, 2.2, "1223", "1", "1", 1, 1, true) + Surface { + SignalStrengthItem( + data = data, + onClick = { }, + ) + } +} + +//@Preview +//@Composable +//private fun InterestsCardLongNamePreview() { +// val data = SignalStrengthReportModel("deviceID2", 123.122, 456.452, "timestamp2", "cellID2", -82, 2) +// Surface { +// SignalStrengthItem( +// data = data, +// onClick = { }, +// ) +// } +// +//} + +//@Preview +//@Composable +//private fun InterestsCardLongDescriptionPreview() { +// val data = SignalStrengthReportModel("deviceID2", 123.122, 456.452, "timestamp2", "cellID2", -82, 2) +// Surface { +// +// SignalStrengthItem( +// data = data, +// onClick = { }, +// ) +// } +//} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/TagIcon.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/TagIcon.kt new file mode 100644 index 0000000..4cfaa2e --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/TagIcon.kt @@ -0,0 +1,56 @@ +package com.lcl.lclmeasurementtool.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.SignalCellularAlt +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun TagIcon(modifier: Modifier, icon: ImageVector) { + Icon(modifier = modifier + .border(width = 2.dp, color = Color.LightGray, shape = RoundedCornerShape(10.dp)) + .padding(2.dp) + .background(MaterialTheme.colorScheme.surface), + imageVector = icon, + contentDescription = null) +} + +@Composable +fun TagLabel(label: String, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .border(BorderStroke(0.8.dp, MaterialTheme.colorScheme.onPrimaryContainer), + shape = RoundedCornerShape(CornerSize(8.dp)) + ) + .padding(4.dp) + + ) { + + Text(text = label, fontSize = 10.sp, fontStyle = FontStyle.Italic) + } +} + +@Preview +@Composable +fun TagLabel_Preview() { + TagLabel(label = "Reported") +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HistoryNavigation.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HistoryNavigation.kt new file mode 100644 index 0000000..1eff478 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HistoryNavigation.kt @@ -0,0 +1,26 @@ +package com.lcl.lclmeasurementtool.ui.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.lcl.lclmeasurementtool.ui.HistoryRoute + +private const val historyGraphRoutePattern = "history_graph" +const val historyRoute = "history_route" + +fun NavController.navigateToHistory(navOptions: NavOptions? = null) { + this.navigate(historyGraphRoutePattern, navOptions) +} + +fun NavGraphBuilder.historyGraph() { + navigation( + route = historyGraphRoutePattern, + startDestination = historyRoute + ) { + composable(route = historyRoute) { + HistoryRoute() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HomeNavigation.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HomeNavigation.kt new file mode 100644 index 0000000..0e3edac --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/HomeNavigation.kt @@ -0,0 +1,20 @@ +package com.lcl.lclmeasurementtool.ui.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.lcl.lclmeasurementtool.MainActivityViewModel +import com.lcl.lclmeasurementtool.ui.HomeRoute + +const val homeNavigationRoute = "home_route" + +fun NavController.navigateToHome(navOptions: NavOptions? = null) { + this.navigate(homeNavigationRoute, navOptions) +} + +fun NavGraphBuilder.homeScreen(isOffline: Boolean, mainViewModel: MainActivityViewModel) { + composable(route = homeNavigationRoute) { + HomeRoute(isOffline, mainViewModel) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/TopLevelDestination.kt b/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/TopLevelDestination.kt new file mode 100644 index 0000000..072880b --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/ui/navigation/TopLevelDestination.kt @@ -0,0 +1,26 @@ +package com.lcl.lclmeasurementtool.ui.navigation + +import com.lcl.lclmeasurementtool.R +import com.lcl.lclmeasurementtool.ui.Icon +import com.lcl.lclmeasurementtool.ui.LCLIcons + + +enum class TopLevelDestination( + val selectedIcon: Icon, + val unselectedIcon: Icon, + val iconTextId: Int, + val titleTextId: Int +){ + HOME( + selectedIcon = Icon.ImageVectorIcon(LCLIcons.Home), + unselectedIcon = Icon.ImageVectorIcon(LCLIcons.HomeBorder), + iconTextId = R.string.home_icon_text, + titleTextId = R.string.home_title_text + ), + HISTORY( + selectedIcon = Icon.ImageVectorIcon(LCLIcons.HistoryData), + unselectedIcon = Icon.ImageVectorIcon(LCLIcons.HistoryDataBorder), + iconTextId = R.string.history_data_icon_text, + titleTextId = R.string.history_data_title_text + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/AnalyticsUtil.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/AnalyticsUtil.kt new file mode 100644 index 0000000..a3db929 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/AnalyticsUtil.kt @@ -0,0 +1,14 @@ +package com.lcl.lclmeasurementtool.util + +class AnalyticsUtil { + companion object { + const val SK = "b501b642-ea97-4b79-b309-012091abdecf" + const val QR_CODE_PARSING_FAILED = "QRCode Parsing Failed" + const val INVALID_KEYS = "Invalid Keys" + + const val REGISTRATION_FAILED = "Registration Failed" + const val LOCATION_NOT_FOUND = "Location Not Found" + const val UPLOAD_FAILED = "Data Upload Failed" + const val DATA_UPLOADED = "Data Uploaded" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/ECDSA.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/ECDSA.kt new file mode 100644 index 0000000..109a40e --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/ECDSA.kt @@ -0,0 +1,195 @@ +package com.lcl.lclmeasurementtool.util + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.io.IOException +import java.math.BigInteger +import java.security.* +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.spec.* +import java.util.* + +/** + * ECDSA helper class that provides convenient helper functions to assist cryptographic operations + * related to EC algorithm + */ +class ECDSA { + + // This is a custom easy helper implementation for the EC prime256v1 or the secp256r1 curves. + // The raw ASN.1 Encoding structures will contain the necessary metadata for interoperation, it would need corresponding + // constant size changes below as necessary to support non 256 bit length curves. + companion object { + + // statically load the BC provider + init { + Security.removeProvider("BC") + Security.addProvider(BouncyCastleProvider()) + } + + // the EC algorithm + private const val ALGORITHM = "EC" + + // SHA256 with ECDSA + private const val SIGNATURE_ALGORITHM = "SHA256withECDSA" + + // public key encoding size + private const val PublicKeyEncodingSize = 65 // 64 + 1 byte 0x04 + + private const val PROVIDER = "BC" + + // padding + private const val PADDING: Byte = 0x04 + + + /** + * Generate the corresponding public key given an EC private key + * + * @param sk the EC private key from which the public key will be generated + * @return an EC public key corresponding to the private key + * @throws IOException if the key gen process failed because of IO issue + * @throws NoSuchAlgorithmException if the key gen process failed because of incorrect algorithm + * @throws InvalidKeySpecException if the key gen process failed because of the invalid private key spec + * @throws NoSuchProviderException if the key gen process failed because of incorrect provider + */ + @Throws(IOException::class, NoSuchAlgorithmException::class, InvalidKeySpecException::class, NoSuchProviderException::class) + fun DerivePublicKey(sk: ECPrivateKey): ECPublicKey { + val kf = getKeyFactory() + val keyParams = sk.params + val pkBytesEmbedded = ByteArray(PublicKeyEncodingSize) + sk.encoded.copyInto(destination = pkBytesEmbedded, destinationOffset = 0, startIndex = sk.encoded.size - PublicKeyEncodingSize, endIndex = sk.encoded.size) + val p = decodePoint(pkBytesEmbedded, keyParams.curve) + val pkSpec = ECPublicKeySpec(p, keyParams) + return kf.generatePublic(pkSpec) as ECPublicKey + } + + /** + * Deserialize the private key from raw bytes + * + * @param raw the raw bytes to convert to private key + * @return an EC private key corresponds to the bytes + * @throws InvalidKeySpecException if the key gen process failed because of invalid key spec + */ + @Throws(InvalidKeySpecException::class) + fun DeserializePrivateKey(raw: ByteArray): ECPrivateKey { + return decodePKCS8ECPrivateKey(raw) + } + + /** + * Deserialize the public key from raw bytes + * + * @param rawSPKIEncoded the raw bytes to convert to public key + * @return an EC public key corresponds to the bytes + * @throws NoSuchAlgorithmException if the key gen process failed because of the incorrect algorithm + * @throws InvalidKeySpecException if the key gen process failed because of the invalid key spec + * @throws NoSuchProviderException if the key gen process failed because of the incorrect provider + */ + @Throws(NoSuchAlgorithmException::class, InvalidKeySpecException::class, + NoSuchProviderException::class) + fun DeserializePublicKey(rawSPKIEncoded: ByteArray): ECPublicKey { + return getKeyFactory().generatePublic(X509EncodedKeySpec(rawSPKIEncoded)) as ECPublicKey + } + + /** + * Verify the signature of the message using the public key + * @param message the message whose signature will be verified + * @param signature the signature to be verified + * @param pk the public key that will be used to verify the signature + * @return true if the signature given matches the one from the message using the public key; + * false otherwise. + * @throws NoSuchAlgorithmException if the verification process failed because of the incorrect algorithm + * @throws InvalidKeyException if the verification process failed because of the invalid key + * @throws SignatureException if the verification process failed because of the invalid signature + * @throws NoSuchProviderException if the verification process failed because of the incorrect provider + */ + @Throws( + NoSuchAlgorithmException::class, + InvalidKeyException::class, + SignatureException::class, + NoSuchProviderException::class + ) + fun Verify(message: ByteArray, signature: ByteArray, pk: ECPublicKey): Boolean { + val s = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER) + s.initVerify(pk) + s.update(message) + return s.verify(signature) + } + + /** + * Sign the message using the private key + * @param message the message to be signed + * @param sk the private key used to sign the message + * @return the signature of the message + * @throws NoSuchAlgorithmException if the signing process failed because of the incorrect algorithm + * @throws InvalidKeyException if the signing process failed because of the invalid key + * @throws SignatureException if the signing process failed because of the invalid signature + * @throws NoSuchProviderException if the signing process failed because of the incorrect provider + */ + @Throws( + NoSuchAlgorithmException::class, + InvalidKeyException::class, + SignatureException::class, + NoSuchProviderException::class + ) + fun Sign(message: ByteArray, sk: ECPrivateKey): ByteArray { + val s = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER) + s.initSign(sk) + s.update(message) + return s.sign() + } + + ///////////////////////////////////// ECUtils ////////////////////////////////////////////// + + /** + * Decode the bytes data using the EC algorithm + * @param data the bytes data to be decoded + * @param curve the EllipticCurve used to decode the data + * @return an EC point corresponds to the raw bytes + * @throws IOException when the data is ill-formatted + */ + @Throws(IOException::class) + private fun decodePoint(data: ByteArray, curve: EllipticCurve): ECPoint { + if (data.isEmpty() || data[0].compareTo(4) != 0) { + throw IOException("Only uncompressed point format supported") + } + + // Per ANSI X9.62, an encoded point is a 1 byte type followed by + // ceiling(log base 2 field-size / 8) bytes of x and the same of y. + val n = (data.size - 1) / 2 + if (n != ((curve.field.fieldSize + 7) shr 3)) { + throw IOException("Point does not match field size") + } + + val xb = data.copyOfRange(1, n + 1) + val yb = data.copyOfRange(n + 1, n + 1 + n) + return ECPoint(BigInteger(1, xb), BigInteger(1, yb)) + } + + /** + * Decode the PKCS8 private key + * @param encoded the encoded bytes of the private key in PKCS8 format + * @return the private key associated with the raw bytes + * @throws InvalidKeySpecException if the key spec is invalid + */ + @Throws(InvalidKeySpecException::class) + private fun decodePKCS8ECPrivateKey(encoded: ByteArray): ECPrivateKey { + val kf = getKeyFactory() + val keySpec = PKCS8EncodedKeySpec(encoded) + return kf.generatePrivate(keySpec) as ECPrivateKey + } + + /** + * Return the key factory for the BC algorithm + * @return a key factor corresponds to the BC algorithm + */ + private fun getKeyFactory(): KeyFactory { + try { + return KeyFactory.getInstance(ALGORITHM, PROVIDER) + } catch (e: NoSuchAlgorithmException) { + throw RuntimeException(e) + } catch (e: NoSuchProviderException) { + throw RuntimeException(e) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/Hex.java b/app/src/main/java/com/lcl/lclmeasurementtool/util/Hex.java new file mode 100644 index 0000000..530011e --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/Hex.java @@ -0,0 +1,442 @@ +// Imported from apache commons-codec. +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.lcl.lclmeasurementtool.util; + +import com.lcl.lclmeasurementtool.errors.DecoderException; +import com.lcl.lclmeasurementtool.errors.EncoderException; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * A utility class for HEX operations (from apache commons) + */ +public class Hex { + + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + public static final String DEFAULT_CHARSET_NAME = StandardCharsets.UTF_8.name(); + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', + 'e', 'f' }; + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', + 'E', 'F' }; + + /** + * Converts a String representing hexadecimal values into an array of bytes of those same values. The returned array + * will be half the length of the passed String, as it takes two characters to represent any given byte. An + * exception is thrown if the passed String has an odd number of elements. + * + * @param data A String containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied char array. + * @throws DecoderException Thrown if an odd number or illegal of characters is supplied + * @since 1.11 + */ + public static byte[] decodeHex(final String data) throws DecoderException { + return decodeHex(data.toCharArray()); + } + + /** + * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The + * returned array will be half the length of the passed array, as it takes two characters to represent any given + * byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param data An array of characters containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied char array. + * @throws DecoderException Thrown if an odd number or illegal of characters is supplied + */ + public static byte[] decodeHex(final char[] data) throws DecoderException { + + final int len = data.length; + + if ((len & 0x01) != 0) { + throw new DecoderException("Odd number of characters."); + } + + final byte[] out = new byte[len >> 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(data[j], j) << 4; + j++; + f = f | toDigit(data[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data a byte[] to convert to Hex characters + * @return A char[] containing lower-case hexadecimal characters + */ + public static char[] encodeHex(final byte[] data) { + return encodeHex(data, true); + } + + /** + * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order. The + * returned array will be double the length of the passed array, as it takes two characters to represent any given + * byte. + * + * @param data a byte buffer to convert to Hex characters + * @return A char[] containing lower-case hexadecimal characters + * @since 1.11 + */ + public static char[] encodeHex(final ByteBuffer data) { + return encodeHex(data, true); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data a byte[] to convert to Hex characters + * @param toLowerCase true converts to lowercase, false to uppercase + * @return A char[] containing hexadecimal characters in the selected case + * @since 1.4 + */ + public static char[] encodeHex(final byte[] data, final boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order. The + * returned array will be double the length of the passed array, as it takes two characters to represent any given + * byte. + * + * @param data a byte buffer to convert to Hex characters + * @param toLowerCase true converts to lowercase, false to uppercase + * @return A char[] containing hexadecimal characters in the selected case + * @since 1.11 + */ + public static char[] encodeHex(final ByteBuffer data, final boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data a byte[] to convert to Hex characters + * @param toDigits the output alphabet (must contain at least 16 chars) + * @return A char[] containing the appropriate characters from the alphabet For best results, this should be either + * upper- or lower-case hex. + * @since 1.4 + */ + protected static char[] encodeHex(final byte[] data, final char[] toDigits) { + final int l = data.length; + final char[] out = new char[l << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return out; + } + + /** + * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order. The + * returned array will be double the length of the passed array, as it takes two characters to represent any given + * byte. + * + * @param byteBuffer a byte buffer to convert to Hex characters + * @param toDigits the output alphabet (must be at least 16 characters) + * @return A char[] containing the appropriate characters from the alphabet For best results, this should be either + * upper- or lower-case hex. + * @since 1.11 + */ + protected static char[] encodeHex(final ByteBuffer byteBuffer, final char[] toDigits) { + return encodeHex(toByteArray(byteBuffer), toDigits); + } + + /** + * Converts an array of bytes into a String representing the hexadecimal values of each byte in order. The returned + * String will be double the length of the passed array, as it takes two characters to represent any given byte. + * + * @param data a byte[] to convert to Hex characters + * @return A String containing lower-case hexadecimal characters + * @since 1.4 + */ + public static String encodeHexString(final byte[] data) { + return new String(encodeHex(data)); + } + + /** + * Converts an array of bytes into a String representing the hexadecimal values of each byte in order. The returned + * String will be double the length of the passed array, as it takes two characters to represent any given byte. + * + * @param data a byte[] to convert to Hex characters + * @param toLowerCase true converts to lowercase, false to uppercase + * @return A String containing lower-case hexadecimal characters + * @since 1.11 + */ + public static String encodeHexString(final byte[] data, final boolean toLowerCase) { + return new String(encodeHex(data, toLowerCase)); + } + + /** + * Converts a byte buffer into a String representing the hexadecimal values of each byte in order. The returned + * String will be double the length of the passed array, as it takes two characters to represent any given byte. + * + * @param data a byte buffer to convert to Hex characters + * @return A String containing lower-case hexadecimal characters + * @since 1.11 + */ + public static String encodeHexString(final ByteBuffer data) { + return new String(encodeHex(data)); + } + + /** + * Converts a byte buffer into a String representing the hexadecimal values of each byte in order. The returned + * String will be double the length of the passed array, as it takes two characters to represent any given byte. + * + * @param data a byte buffer to convert to Hex characters + * @param toLowerCase true converts to lowercase, false to uppercase + * @return A String containing lower-case hexadecimal characters + * @since 1.11 + */ + public static String encodeHexString(final ByteBuffer data, final boolean toLowerCase) { + return new String(encodeHex(data, toLowerCase)); + } + + private static byte[] toByteArray(final ByteBuffer byteBuffer) { + if (byteBuffer.hasArray()) { + return byteBuffer.array(); + } + final byte[] byteArray = new byte[byteBuffer.remaining()]; + byteBuffer.get(byteArray); + return byteArray; + } + + /** + * Converts a hexadecimal character to an integer. + * + * @param ch A character to convert to an integer digit + * @param index The index of the character in the source + * @return An integer + * @throws DecoderException Thrown if ch is an illegal hex character + */ + protected static int toDigit(final char ch, final int index) throws DecoderException { + final int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new DecoderException("Illegal hexadecimal character " + ch + " at index " + index); + } + return digit; + } + + private final Charset charset; + + /** + * Creates a new codec with the default charset name {@link #DEFAULT_CHARSET} + */ + public Hex() { + // use default encoding + this.charset = DEFAULT_CHARSET; + } + + /** + * Creates a new codec with the given Charset. + * + * @param charset the charset. + * @since 1.7 + */ + public Hex(final Charset charset) { + this.charset = charset; + } + + /** + * Creates a new codec with the given charset name. + * + * @param charsetName the charset name. + * @throws java.nio.charset.UnsupportedCharsetException If the named charset is unavailable + * @since 1.4 + * @since 1.7 throws UnsupportedCharsetException if the named charset is unavailable + */ + public Hex(final String charsetName) { + this(Charset.forName(charsetName)); + } + + /** + * Converts an array of character bytes representing hexadecimal values into an array of bytes of those same values. + * The returned array will be half the length of the passed array, as it takes two characters to represent any given + * byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param array An array of character bytes containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied byte array (representing characters). + * @throws DecoderException Thrown if an odd number of characters is supplied to this function + * @see #decodeHex(char[]) + */ + public byte[] decode(final byte[] array) throws DecoderException { + return decodeHex(new String(array, getCharset()).toCharArray()); + } + + /** + * Converts a buffer of character bytes representing hexadecimal values into an array of bytes of those same values. + * The returned array will be half the length of the passed array, as it takes two characters to represent any given + * byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param buffer An array of character bytes containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied byte array (representing characters). + * @throws DecoderException Thrown if an odd number of characters is supplied to this function + * @see #decodeHex(char[]) + * @since 1.11 + */ + public byte[] decode(final ByteBuffer buffer) throws DecoderException { + return decodeHex(new String(toByteArray(buffer), getCharset()).toCharArray()); + } + + /** + * Converts a String or an array of character bytes representing hexadecimal values into an array of bytes of those + * same values. The returned array will be half the length of the passed String or array, as it takes two characters + * to represent any given byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param object A String, ByteBuffer, byte[], or an array of character bytes containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied byte array (representing characters). + * @throws DecoderException Thrown if an odd number of characters is supplied to this function or the object is not + * a String or char[] + * @see #decodeHex(char[]) + */ + public Object decode(final Object object) throws DecoderException { + if (object instanceof String) { + return decode(((String) object).toCharArray()); + } else if (object instanceof byte[]) { + return decode((byte[]) object); + } else if (object instanceof ByteBuffer) { + return decode((ByteBuffer) object); + } else { + try { + return decodeHex((char[]) object); + } catch (final ClassCastException e) { + throw new DecoderException(e.getMessage(), e); + } + } + } + + /** + * Converts an array of bytes into an array of bytes for the characters representing the hexadecimal values of each + * byte in order. The returned array will be double the length of the passed array, as it takes two characters to + * represent any given byte. + *

+ * The conversion from hexadecimal characters to the returned bytes is performed with the charset named by + * {@link #getCharset()}. + *

+ * + * @param array a byte[] to convert to Hex characters + * @return A byte[] containing the bytes of the lower-case hexadecimal characters + * @since 1.7 No longer throws IllegalStateException if the charsetName is invalid. + * @see #encodeHex(byte[]) + */ + public byte[] encode(final byte[] array) { + return encodeHexString(array).getBytes(this.getCharset()); + } + + /** + * Converts byte buffer into an array of bytes for the characters representing the hexadecimal values of each byte + * in order. The returned array will be double the length of the passed array, as it takes two characters to + * represent any given byte. + *

+ * The conversion from hexadecimal characters to the returned bytes is performed with the charset named by + * {@link #getCharset()}. + *

+ * + * @param array a byte buffer to convert to Hex characters + * @return A byte[] containing the bytes of the lower-case hexadecimal characters + * @see #encodeHex(byte[]) + * @since 1.11 + */ + public byte[] encode(final ByteBuffer array) { + return encodeHexString(array).getBytes(this.getCharset()); + } + + /** + * Converts a String or an array of bytes into an array of characters representing the hexadecimal values of each + * byte in order. The returned array will be double the length of the passed String or array, as it takes two + * characters to represent any given byte. + *

+ * The conversion from hexadecimal characters to bytes to be encoded to performed with the charset named by + * {@link #getCharset()}. + *

+ * + * @param object a String, ByteBuffer, or byte[] to convert to Hex characters + * @return A char[] containing lower-case hexadecimal characters + * @throws EncoderException Thrown if the given object is not a String or byte[] + * @see #encodeHex(byte[]) + */ + public Object encode(final Object object) throws EncoderException { + byte[] byteArray; + if (object instanceof String) { + byteArray = ((String) object).getBytes(this.getCharset()); + } else if (object instanceof ByteBuffer) { + byteArray = toByteArray((ByteBuffer) object); + } else { + try { + byteArray = (byte[]) object; + } catch (final ClassCastException e) { + throw new EncoderException(e.getMessage(), e); + } + } + return encodeHex(byteArray); + } + + /** + * Gets the charset. + * + * @return the charset. + * @since 1.7 + */ + public Charset getCharset() { + return this.charset; + } + + /** + * Gets the charset name. + * + * @return the charset name. + * @since 1.4 + */ + public String getCharsetName() { + return this.charset.name(); + } + + /** + * Returns a string representation of the object, which includes the charset name. + * + * @return a string representation of the object. + */ + @Override + public String toString() { + return super.toString() + "[charsetName=" + this.charset + "]"; + } +} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/NetworkUtil.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/NetworkUtil.kt new file mode 100644 index 0000000..79b264d --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/NetworkUtil.kt @@ -0,0 +1,14 @@ +package com.lcl.lclmeasurementtool.util + +import com.lcl.lclmeasurementtool.model.datamodel.BaseMeasureDataModel +import com.lcl.lclmeasurementtool.model.datamodel.MeasurementReportModel +import com.lcl.lclmeasurementtool.model.datamodel.UserData +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +fun prepareReportData(measureDataModel: BaseMeasureDataModel, userData: UserData): String { + val serialized = Json.encodeToString(measureDataModel).toByteArray() + val sig_m = ECDSA.Sign(serialized, ECDSA.DeserializePrivateKey(userData.skT.toByteArray())) + val report = MeasurementReportModel(Hex.encodeHexString(sig_m), Hex.encodeHexString(userData.hPKR.toByteArray()), Hex.encodeHexString(serialized), userData.showData) + return Json.encodeToString(report) +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/SecurityUtil.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/SecurityUtil.kt new file mode 100644 index 0000000..d219027 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/SecurityUtil.kt @@ -0,0 +1,32 @@ +package com.lcl.lclmeasurementtool.util + +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +class SecurityUtil { + companion object { + // RSA algorithm + const val RSA = "RSA" + + // SHA256 with RSA signature + const val SHA_256_WITH_RSA_SIGNATURE = "SHA256withRSA" + + // SHA-256 + const val SHA_256_HASH = "SHA-256" + + /** + * Digest the message with given algorithm + * + * @param data the data to be hashed + * @param algorithm the algorithm used to digest the message + * @return the digested message in byte array + * @throws NoSuchAlgorithmException if the provided algorithm doesn't exist + */ + @Throws(NoSuchAlgorithmException::class) + fun digest(data: ByteArray, algorithm: String): ByteArray { + val messageDigest = MessageDigest.getInstance(algorithm) + messageDigest.update(data) + return messageDigest.digest() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/SyncUtil.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/SyncUtil.kt new file mode 100644 index 0000000..791fd13 --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/SyncUtil.kt @@ -0,0 +1,35 @@ +package com.lcl.lclmeasurementtool.util + +import android.util.Log +import retrofit2.HttpException +import kotlin.coroutines.cancellation.CancellationException + +private suspend fun suspendRunCatching(block: suspend () -> T): Result = try { + Log.d("suspendRunCatching", "block ${block.javaClass.name} is invoked") + Result.success(block()) +} catch (cancellationException: CancellationException) { + throw cancellationException +} catch (httpException: HttpException) { + Log.d( + "suspendRunCatching", + "HttpException occurred. Returning failure Result", + httpException + ) + Result.failure(httpException) +} catch (exception: Exception) { + Log.d( + "suspendRunCatching", + "Failed to evaluate a suspendRunCatchingBlock. Returning failure Result", + exception + ) + Result.failure(exception) +} + +interface Syncable { + suspend fun syncWith(synchronizer: Synchronizer): Boolean +} + +interface Synchronizer { + suspend fun Syncable.sync() = this@sync.syncWith(this@Synchronizer) + suspend fun syncData(action: suspend () -> Unit) = suspendRunCatching { action() }.isSuccess +} diff --git a/app/src/main/java/com/lcl/lclmeasurementtool/util/Time.kt b/app/src/main/java/com/lcl/lclmeasurementtool/util/Time.kt new file mode 100644 index 0000000..39389bf --- /dev/null +++ b/app/src/main/java/com/lcl/lclmeasurementtool/util/Time.kt @@ -0,0 +1,10 @@ +package com.lcl.lclmeasurementtool.util + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class TimeUtil { + companion object { + fun getCurrentTime(): String = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).toString() + } +} \ No newline at end of file diff --git a/app/src/main/proto/user_preferences.proto b/app/src/main/proto/user_preferences.proto new file mode 100644 index 0000000..ea67a1b --- /dev/null +++ b/app/src/main/proto/user_preferences.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +option java_package = "com.lcl.lclmeasurementtool.datastore"; +option java_multiple_files = true; + +message UserPreferences { + bool show_data = 1; + bool logged_in = 2; + bytes h_pkr = 3; + bytes sk_t = 4; + bytes R = 5; + string device_id = 6; +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/cellular_strength.xml b/app/src/main/res/drawable/cellular_strength.xml deleted file mode 100644 index 51afcb1..0000000 --- a/app/src/main/res/drawable/cellular_strength.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml deleted file mode 100644 index f03861e..0000000 --- a/app/src/main/res/drawable/download.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/lcl_purple_gold_uw.png b/app/src/main/res/drawable/lcl_purple_gold_uw.png new file mode 100644 index 0000000000000000000000000000000000000000..ab6de36ec9e80b25e721b6b18dae4e32cf05ba5c GIT binary patch literal 88097 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 - - diff --git a/app/src/main/res/drawable/signal_strength_indicator.xml b/app/src/main/res/drawable/signal_strength_indicator.xml deleted file mode 100644 index fc2ab4f..0000000 --- a/app/src/main/res/drawable/signal_strength_indicator.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/start.xml b/app/src/main/res/drawable/start.xml deleted file mode 100644 index 2d983be..0000000 --- a/app/src/main/res/drawable/start.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/stop.xml b/app/src/main/res/drawable/stop.xml deleted file mode 100644 index 846b883..0000000 --- a/app/src/main/res/drawable/stop.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/upload.xml b/app/src/main/res/drawable/upload.xml deleted file mode 100644 index 7565869..0000000 --- a/app/src/main/res/drawable/upload.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 01eb776..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,223 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/stats_template.xml b/app/src/main/res/layout/stats_template.xml deleted file mode 100644 index 581f9bc..0000000 --- a/app/src/main/res/layout/stats_template.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml deleted file mode 100644 index 5c622e3..0000000 --- a/app/src/main/res/menu/menu_main.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cf..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cf..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a571e60098c92c2baca8a5df62f2929cbff01b52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3593 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 P00000NkvXXu0mjfP=x?Y diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 61da551c5594a1f9d26193983d2cd69189014603..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5339 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 QOaK4?07*qoM6N<$f(TKQr2qf` literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index c41dd28531901b2c23927768c84bb6765ebcc1db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2636 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 6dba46dab19242bf475ddf2e0a10042de6a0be16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4926 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index b216f2d313cc673d8b8c4da591c174ebed52795c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11873 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index f25a419744727fb48b4b4137b6c6a3a8f2620d51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10652 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/icon.png b/app/src/main/res/mipmap-xxxhdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..459f6e75caae8c692b9a5746a70ffe0125662753 GIT binary patch literal 39020 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�nTX!^n%Xl5h!nbl`1i9>0;fcZcESJKKvg~dG_QK zKZ*|gslZPd`!f-M^%+PoJvXoTUZ}M?=PzBq-OuXp^b1zxhJ`D_eGELhI27 z(jNlxuakZj0PRc+ioTmvXh=h>sDOhjdMo8KM2?d!acnr31pbRKx37NMLG{O6+pK<= z`wam8r=6hr|Alr>NB+}Req)^eYy@Dv1`;d)r$6rUaWC1wnYJ_ofuwnwq&)FPl6v#4 zaZ(35(S-><>6v3pF!Z*h%)M9AovURRyw+v7*J?T4y+#UMJD@jIA#LN~p&6auTTmd2 z;WH-{emuLGFN18TttQ7mqHHHV$r965836mmc2@+gOOoUao{&O#Z&UrLgDT_r!}|@r0K)of*g{1o$}~toTGFA^~N*>Bv_+ z{?b=(r)3tH-+Xo~qOz*3&$&2QjZf~RJO?)-TXs}Ku?h?#0HOtqoT#>=UPVHK;h+mH zPXXT7NJzL29#omdEUWB8(h+QEt44M=g*>~DsQ zzAi%A$HUPp{A#wO--)bNt2f+&ED_g^^U!vy6xrs<=*v1tpPtDQ8y%)O1+K*of-@4y z$%VU+x$DHSs}}&e6I{Z~kO3E2Wk8=!n$zEKc%1XKTHxQaN;YiSE#FRBFZR3?NkH`X zhQ@daCj^3;R7ys-0cRI9TD|1TD7p3FIMw0d_sXA^`GF8V+#%>4hRu} zE5n({FFbnp<RZq5RTeKQ#d?YeDkpF^&@IAfmy4DX(hrt! zkGmD=bCNWa;?Q88?izJ9VNL-4;W0x0Eu>_l9A_KrgLwJ;;NrLdK6CRcM z?2!yi4%V#WdCge$q46*0oE-`uPw^^5oQkJGQ}r%*`R%C{&%HUB^!~%htC30a5bzH* zYPj|1B~IVTmJEi7uGF48yMLDDdcVdm0*eh zTaqAue5WXMcDo<<=yN(IYqS-XDsg?78+31vulb1$)8_`Za#AMh2d&#xl7r(vlc4EptldJlKe+> zQCqvYejo8))7qOpt%LD_D=h)~z#Rg=o{eiQpKV&lANifT6<2*tOpOI#_^1eZ;fC422xS~OUHWz48U)I=(iTD ze#caQdHDH6x%%c9)oL>-aMJ5UK=nO$JUXG~)d|zKAleO8nHM1&Fm}S}{ez8((YS-% zI~lGK5!l8OqG|TB;-d2oY|@0llNbx^y`%xTUTEn;AMs1(0r#l z^~)mV`>)};uL`sh_5|(-X06i5Vo()ms>@d(o);r@I~a01EUy+vmSxSfe4unb=Kr!1>syJ zufze-DJGAulaVwrB=?j5C2hSnJX+8~k3!qnFbkqtM#%6h!U6a`iU$Oz)yv0SQdXJ& z>BL2L1WvCkDy^J^;d>vs8oBAboHC<&Dk6X}xJ_4@cwD@#O{<9W?;1POa^W5t35q<)159cAvw`khK&xF z^DYikqgz`PH)5z+%~m&}!sk!?{wMoeAI_@F&Mlgchu=lsgUr-)F7zv>$lp^L0XW2L zKL_8?C4tj#xizV6bpP9~yU6FV5d#7u!h)h4PKUP%Az3TSUFzDOs+4(CN@e}eRZ&U@3Ts749 zicte>aWN5La474inp`kG@u<7g>Sg)-a%I{4GTEI$N8pIP)dU&HG@lk^5=MsP4FR7( z-_Qv37OWddDZVmbXow8DAVf{<;EP}wc+vy!+$JwOM4H7of2sTA+XZ%<#Vo~meu}&q znTdf=&iAcac8Hmsx->(FZF!a*l{lfo#$zV&@jtQaAN(|P*k0L|~ z{1d4=H``_T!g6KZ@=Dp0#fSh@c*D+s%tId*$Wu?i&&j{mp|RR3k3VcKRWZ2AJCS|R zpSU)@(raKK0bfn+ipT@Oqyu#q^9@dS#g^?^zo zcDxn@n}h_|5cB|v)3bk|n$pRpkvP;T!m%NGWUAd)SYEn#<)+$irY@`hapjgQAp2sx zIu*G9nd!6xe7#%xYH55w9Y0$l0)G!i9@#27y!VCYo)H0!Qw#L@eS-u2f`J4+jB!i9 z_b}d2^Y=Pr$Hp4jy0%71+gdAm_+1Tz;PS_U^zaZ2NTHJ%xTGZ_1a}N0HTh~>I;u|t z!1*xsotpk;`H)nC`OyL1Xj32nK03}S9Xk6-mp%b%YIi?`lC_AI0pJBPiT-(Ns@&?n+y-?|dYzK7&8}o+*d;f! zUdl@m&x~p96BV!RmH_%Ig;-Ka(g^_1qoMkU5tE$?#J+pqn(v1xZ zmu{*~%go(@Di$FxM9xJv(kai}8XY;(=b`4}NbjA>_X(R!#wy3}iO6Ro4{RM9meRj> zYT%H5U48oWNU^q#jr8+})jmici$*FTz&Fe|!AL_jpvt9Il(>i(Nog}{9vM;u(G=^su9W>$EpM5G~bYb2KyWS7+>{%{@^8s-h3qTUkK$|5ms ztZG!Oh2Pb%C~qYI!0wG_%{=3P*pi`(phDeHZLhD%$t|k+Wn)IePb;@HtX!K`mYr9$ z3w5tRUW!a}L?az+T#S3C?dM++0gTCH-$}`EQRQN5q*#3EjoioA*V-BWOj~v9m=MtW zjARQwB{nK7AS^h5@i2UFj^ITdelan`5>{y-26gul6G0sH9{9J%&Qa}F>uMlpx4RWO zxoxN?LLlTFqU9K;(v-(DFoeYip)mmPKE6;hu$fd}e*k}gS+U}`kBn!=@d(Yqsvp&aJC!pxBbS_#|`kk&VRu za~$-|#<=(W*D8Fgi4@2kwoE3w0IZlTldCheYyOwxuq4_U({ zd>Y9hZ~n3?y)h6iKmUgSEk~sJZ;nq>gaEyB&=iQPk;ttZKjZL;IGHfs6$$b8w?W#j z_+b4;+;t!U+#Q(#*4~Xe+fJBKxsg#5r1pvn&p-(i@!xCgw4$nL%*FW3K`ufj(DSnj o`Cy5N#$*6|ON7wx{_n5+KmHSY?VVH4{{R3007*qoM6N<$f?8xCkpKVy literal 0 HcmV?d00001 diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000..22d7f00 --- /dev/null +++ b/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,3 @@ + + 48dp + \ No newline at end of file diff --git a/app/src/main/res/values-w1240dp/dimens.xml b/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 0000000..d73f4a3 --- /dev/null +++ b/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ + + 200dp + \ No newline at end of file diff --git a/app/src/main/res/values-w600dp/dimens.xml b/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 0000000..22d7f00 --- /dev/null +++ b/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ + + 48dp + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..6cf9ed4 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,12 @@ + + + + Reply + Reply to all + + + + reply + reply_all + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 125df87..8835c95 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,3 +1,5 @@ 16dp + 12dp + 10dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c1b934..c826a4a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,20 +1,80 @@ - LCL Measurement Tool + SCN Measurement Tool Settings First Fragment Second Fragment Next Previous - + Error: Missing SIM card + The measurement requires a Seattle Community Network SIM card. Please insert your SIM card before proceeding. Hello first fragment Hello second fragment. Arg: %1$s - "We use Location Service to provide more accurate core network measurement and other related service. Granting the permission of the location service will help us improve our service. We will upload anonymized and fused location data to our encrypted server for post-analysis to better network experience." + "We use Location Services to provide more accurate network measurements of our network coverage. We will upload anonymous and aggregated location data to our encrypted server for analysis to help us serve more people in our community. See https://seattlecommunitynetwork.org/ for more details." Settings - No location detected - The core functionality relies on location to provide better cellular network experience. Disabling the location service will result in missing in functionality. + Error: No location detected. + Please enable Location Services to enable core measurement functionality! Settings - Using Location Service - "We use Location Service to provide core functionalities in this measurement tool. Disabling location service will result in missing in functionality." + Using Your Location and Network + "We use Location Services and Network to provide core functionality in this measurement tool. Disabling them will result in missing functionality." uuid - \ No newline at end of file + Turn Cellular Data On + These tests rely on a connection to a cellular network. Please enable cellular data and then proceed with the tests. + Error: An error occurred while testing connection speed. + Error + device_id + PING + IPERF_UP + IPERF_DOWN + Validating … + Cannot validate authentication code. Please retry or contact the administrator at lcl@seattlecommunitynetwork.org. + Validation success. + Registration failed. Please try again. + The QR Code is invalid. Please rescan the code or contact the administrator at lcl@seattlecommunitynetwork.org. + Authentication keys are compromised. Please rescan the QR Code. + Error: Authentication key information is missing. + Test canceled. + Location is not available. Test canceled. + Test started. + Test completed. + Unknown test. + Cannot connect to the server. Please retry or contact the administrator at lcl@seattlecommunitynetwork.org. + Data upload failed. Please contact the administrator at lcl@seattlecommunitynetwork.org. + Key information is missing + MainActivity2 + + Messages + Sync + + + Your signature + Default reply action + + + Sync email periodically + Download incoming attachments + Automatically download attachments for incoming emails + + Only download attachments when manually requested + + + Settings + Settings + Loading… + Privacy Policy + Licenses + Brand Guidelines + Feedback + Theme + Default + Android + System default + Light + Dark + OK + + Home + Home + History + History + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 53bf906..0b07ed5 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,16 +3,17 @@ @@ -21,6 +22,10 @@ true + +