From 770b0f080906092f0992ddb91b26f8d390818941 Mon Sep 17 00:00:00 2001 From: Foxpace Date: Fri, 28 Aug 2026 23:51:04 +0200 Subject: [PATCH 1/2] refactor: apply recording domain glossary --- AGENTS.MD | 45 +++++-- CONTEXT.md | 48 ++++++- README.md | 20 +-- .../files/GooglePlayWearFileTransferClient.kt | 2 +- .../wearoslib/protocol/WearCommand.kt | 6 +- .../wearoslib/protocol/WearCommandCodec.kt | 1 + .../protocol/WearCommandCodecTest.kt | 14 +- .../PhoneSensorRecordingEmulatorTest.kt | 2 +- .../PhoneWearDiscoveryEmulatorTest.kt | 8 +- .../emulator/RecordingEmulatorFixture.kt | 54 ++++---- .../main/OnboardingScreenRobot.kt | 6 +- .../presentation/main/OnboardingScreenTest.kt | 6 +- .../presentation/main/RecordScreenRobot.kt | 10 +- .../presentation/main/RecordScreenTest.kt | 10 +- ...nRobot.kt => RecordingSetupScreenRobot.kt} | 14 +- ...eenTest.kt => RecordingSetupScreenTest.kt} | 18 +-- .../main/SensorDetailsScreenTest.kt | 4 +- .../com/tomasrepcik/sensorbox/MsgListener.kt | 8 +- .../sensorbox/activities/MainActivity.kt | 28 ++-- .../AndroidMeasurementRepository.kt | 4 +- .../sensorbox/di/RecordingModule.kt | 28 ++-- .../measurement/DocumentStorageUseCase.kt | 22 ---- .../paired/PairedRecordingCoordinator.kt | 32 ++--- .../domain/paired/PairedRecordingModel.kt | 22 ++-- .../paired/PhoneRecordingSessionObserver.kt | 24 ++-- ...Handler.kt => PhoneWatchCommandHandler.kt} | 35 ++--- ...cher.kt => PhoneWatchMessageDispatcher.kt} | 4 +- ...tInbox.kt => WatchRecordingResultInbox.kt} | 16 +-- .../PhoneRecordingController.kt | 55 ++++---- .../recording/RecordingArchiveRepository.kt | 23 ++++ .../RecordingControlUseCase.kt} | 8 +- .../RecordingPermissionUseCase.kt} | 16 +-- .../RecordingSetup.kt} | 18 +-- ...logStore.kt => WatchSensorCatalogStore.kt} | 2 +- ...eUseCase.kt => ReceiveWatchFileUseCase.kt} | 6 +- ...Destination.kt => WatchFileDestination.kt} | 10 +- ...mentScreen.kt => ActiveRecordingScreen.kt} | 24 ++-- .../presentation/main/MainContract.kt | 8 +- .../presentation/main/MainViewModel.kt | 4 +- .../presentation/main/OnboardingContract.kt | 10 +- .../presentation/main/OnboardingScreen.kt | 48 +++---- .../presentation/main/OnboardingViewModel.kt | 18 +-- .../presentation/main/RecordScreen.kt | 38 +++--- .../presentation/main/RecordingContract.kt | 94 +++++++------- ...SetupScreen.kt => RecordingSetupScreen.kt} | 42 +++--- .../presentation/main/RecordingViewModel.kt | 122 +++++++++--------- .../presentation/main/SensorBoxApp.kt | 28 ++-- .../presentation/main/SensorBoxComponents.kt | 2 +- .../presentation/main/SensorDetailsScreen.kt | 18 +-- .../presentation/main/SensorPreviewScreen.kt | 2 +- .../presentation/main/SettingsContract.kt | 2 +- .../presentation/main/SettingsScreen.kt | 16 +-- .../presentation/main/SettingsViewModel.kt | 8 +- app/src/main/res/values/strings.xml | 58 ++++----- .../main/ReadmePhoneScreenshotPreviews.kt | 12 +- .../paired/PairedRecordingCoordinatorTest.kt | 60 ++++----- ...DocumentRecordingArchiveRepositoryTest.kt} | 16 +-- .../RecordingPermissionUseCaseTest.kt} | 10 +- .../RecordingSetupTest.kt} | 14 +- ...Test.kt => WatchSensorCatalogStoreTest.kt} | 10 +- .../presentation/main/FeatureViewModelTest.kt | 120 ++++++++--------- .../presentation/main/RecordingReducerTest.kt | 20 +-- .../main/RecordingStateConversionTest.kt | 2 +- build.gradle.kts | 2 +- .../sensorbox/core/error/AppError.kt | 2 +- .../core/preferences/AppPreferences.kt | 2 +- .../core/preferences/AppPreferencesIntent.kt | 2 +- .../core/preferences/AppPreferencesReducer.kt | 4 +- .../DataStoreAppPreferencesRepository.kt | 4 +- .../preferences/AppPreferencesReducerTest.kt | 6 +- .../DataStoreAppPreferencesRepositoryTest.kt | 2 +- ...-recording-engine-from-android-adapters.md | 2 +- ...own-presentation-state-by-user-workflow.md | 4 +- ...fine-sensorbox-application-architecture.md | 4 +- docs/recording-architecture-guided-review.md | 40 +++--- .../android/en-GB/full_description.txt | 14 +- .../sensorbox/recording/RecordingEngine.kt | 40 +++--- .../sensorbox/recording/RecordingModel.kt | 8 +- .../recording/RecordingEngineTest.kt | 34 ++--- sensorservices/src/main/AndroidManifest.xml | 2 +- .../sensorservices/handlers/GPSHandler.kt | 4 +- .../ActivityRecognitionPlatform.kt | 12 +- ...ent.kt => ActivityRecognitionRecording.kt} | 16 +-- .../{GPSMeasurement.kt => GpsRecording.kt} | 18 +-- ...andler.kt => MeasurementMetadataWriter.kt} | 14 +- ...ensorMeasurement.kt => SensorRecording.kt} | 20 +-- ...otion.kt => SignificantMotionRecording.kt} | 18 +-- .../intent/MeasurementIntentFactory.kt | 45 ------- .../intent/MeasurementLaunchRequest.kt | 56 -------- .../intent/RecordingIntentFactory.kt | 45 +++++++ .../sensorservices/intent/RecordingRequest.kt | 56 ++++++++ .../ActivityRecordingSource.kt | 14 +- .../AndroidRecordingSources.kt | 26 ++-- .../serviceController/GpsRecordingSource.kt | 16 +-- .../SensorRecordingSource.kt | 16 +-- .../serviceController/ServiceController.kt | 14 +- .../ServiceControllerFactory.kt | 4 +- ...e.kt => SessionMetadataRecordingSource.kt} | 41 +++--- .../SignificantMotionRecordingSource.kt | 14 +- .../sensorservices/services/Notify.kt | 6 +- ...Resources.kt => RecordingHostResources.kt} | 20 +-- ...surementService.kt => RecordingService.kt} | 60 ++++----- .../session/MeasurementSessionStore.kt | 48 ------- ...ssionState.kt => RecordingSessionState.kt} | 8 +- .../session/RecordingSessionStore.kt | 52 ++++++++ .../sensorservices/types/SensorHolder.kt | 2 +- .../sensorservices/types/SensorSpec.kt | 2 +- .../src/main/res/values/strings.xml | 2 +- ...kt => ActivityRecognitionRecordingTest.kt} | 14 +- ...ionTest.kt => RecordingSourceStartTest.kt} | 10 +- .../emulator/WearRecordingEmulatorFixture.kt | 14 +- .../WearToPhoneSyncSenderEmulatorTest.kt | 6 +- .../PhoneRecordingResultInbox.kt | 14 +- .../communication/WearCommandEnvironment.kt | 22 ++-- .../communication/WearCommandHandler.kt | 38 +++--- .../WearRecordingSessionObserver.kt | 24 ++-- .../sensorbox/di/RecordingModule.kt | 10 +- .../WatchRecordingControlUseCase.kt} | 28 ++-- .../WatchRecordingPermissionUseCase.kt} | 8 +- ...rsUseCase.kt => GetWatchSensorsUseCase.kt} | 8 +- ...Descriptor.kt => WatchSensorDescriptor.kt} | 4 +- ...ase.kt => SyncWatchMeasurementsUseCase.kt} | 8 +- .../sensorbox/presentation/WearComponents.kt | 6 +- .../dashboard/WearDashboardContract.kt | 16 +-- .../dashboard/WearDashboardScreen.kt | 30 ++--- .../dashboard/WearDashboardViewModel.kt | 42 +++--- .../phonelaunch/PhoneLaunchScreen.kt | 4 +- .../dashboard/ReadmeWearScreenshotPreviews.kt | 4 +- .../communication/WearCommandHandlerTest.kt | 8 +- .../WearMeasurementPermissionUseCaseTest.kt | 23 ---- .../WatchRecordingPermissionUseCaseTest.kt | 23 ++++ ...orTypesTest.kt => WatchSensorTypesTest.kt} | 4 +- .../dashboard/WearDashboardReducerTest.kt | 8 +- 133 files changed, 1356 insertions(+), 1276 deletions(-) rename app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/{MeasurementSetupScreenRobot.kt => RecordingSetupScreenRobot.kt} (66%) rename app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/{MeasurementSetupScreenTest.kt => RecordingSetupScreenTest.kt} (73%) delete mode 100644 app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/DocumentStorageUseCase.kt rename app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/{PhoneWearCommandHandler.kt => PhoneWatchCommandHandler.kt} (72%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/{PhoneWearMessageDispatcher.kt => PhoneWatchMessageDispatcher.kt} (88%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/{WearRecordingResultInbox.kt => WatchRecordingResultInbox.kt} (68%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/{measurement => recording}/PhoneRecordingController.kt (54%) create mode 100644 app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingArchiveRepository.kt rename app/src/main/java/com/tomasrepcik/sensorbox/domain/{measurement/MeasurementControlUseCase.kt => recording/RecordingControlUseCase.kt} (73%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/{measurement/MeasurementPermissionUseCase.kt => recording/RecordingPermissionUseCase.kt} (51%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/{measurement/MeasurementRequest.kt => recording/RecordingSetup.kt} (78%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/{WearSensorCatalogStore.kt => WatchSensorCatalogStore.kt} (94%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/{ReceiveWearFileUseCase.kt => ReceiveWatchFileUseCase.kt} (91%) rename app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/{WearFileDestination.kt => WatchFileDestination.kt} (91%) rename app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/{ActiveMeasurementScreen.kt => ActiveRecordingScreen.kt} (91%) rename app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/{MeasurementSetupScreen.kt => RecordingSetupScreen.kt} (87%) rename app/src/test/java/com/tomasrepcik/sensorbox/domain/{measurement/DocumentStorageUseCaseTest.kt => recording/DocumentRecordingArchiveRepositoryTest.kt} (76%) rename app/src/test/java/com/tomasrepcik/sensorbox/domain/{measurement/MeasurementPermissionUseCaseTest.kt => recording/RecordingPermissionUseCaseTest.kt} (78%) rename app/src/test/java/com/tomasrepcik/sensorbox/domain/{measurement/MeasurementRequestTest.kt => recording/RecordingSetupTest.kt} (79%) rename app/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/{WearSensorCatalogStoreTest.kt => WatchSensorCatalogStoreTest.kt} (59%) rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/{ActivityRecognitionMeasurement.kt => ActivityRecognitionRecording.kt} (87%) rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/{GPSMeasurement.kt => GpsRecording.kt} (86%) rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/{ExtraInfoHandler.kt => MeasurementMetadataWriter.kt} (93%) rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/{SensorMeasurement.kt => SensorRecording.kt} (83%) rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/{SignificantMotion.kt => SignificantMotionRecording.kt} (83%) delete mode 100644 sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementIntentFactory.kt delete mode 100644 sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementLaunchRequest.kt create mode 100644 sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingIntentFactory.kt create mode 100644 sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingRequest.kt rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/{SessionRecordingSource.kt => SessionMetadataRecordingSource.kt} (62%) rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/{MeasurementHostResources.kt => RecordingHostResources.kt} (85%) rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/{MeasurementService.kt => RecordingService.kt} (75%) delete mode 100644 sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/MeasurementSessionStore.kt rename sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/{MeasurementSessionState.kt => RecordingSessionState.kt} (60%) create mode 100644 sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/RecordingSessionStore.kt rename sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/{ActivityRecognitionMeasurementTest.kt => ActivityRecognitionRecordingTest.kt} (90%) rename sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/{MeasurementPreparationTest.kt => RecordingSourceStartTest.kt} (88%) rename wear/src/main/java/com/tomasrepcik/sensorbox/domain/{measurement/WearMeasurementControlUseCase.kt => recording/WatchRecordingControlUseCase.kt} (74%) rename wear/src/main/java/com/tomasrepcik/sensorbox/domain/{measurement/WearMeasurementPermissionUseCase.kt => recording/WatchRecordingPermissionUseCase.kt} (59%) rename wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/{GetWearSensorsUseCase.kt => GetWatchSensorsUseCase.kt} (83%) rename wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/{WearSensorDescriptor.kt => WatchSensorDescriptor.kt} (89%) rename wear/src/main/java/com/tomasrepcik/sensorbox/domain/sync/{SyncWearMeasurementsUseCase.kt => SyncWatchMeasurementsUseCase.kt} (90%) delete mode 100644 wear/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementPermissionUseCaseTest.kt create mode 100644 wear/src/test/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingPermissionUseCaseTest.kt rename wear/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/{WearSensorTypesTest.kt => WatchSensorTypesTest.kt} (62%) diff --git a/AGENTS.MD b/AGENTS.MD index a82ab8c..7ab7ccb 100644 --- a/AGENTS.MD +++ b/AGENTS.MD @@ -23,8 +23,8 @@ Every screen is its own module and cross-cutting concerns are in their own modul The modules are small and have a single responsibility. Every screen has root composable, where the viewmodel and UI meet ends. The UI only talks to viewmodel, not repos. Repos and usecases are part of the viewmodels. -UI is dumb and only renders the state. Root is responsible for delegating actions, events and -native side effects - it can use other classes to do so. +UI is dumb and only renders the state. The root composable delegates intents and performs +native side effects. It can use other classes to do so. ## Testing @@ -42,15 +42,36 @@ The tests should be simple and readable, ideally only one line per statement in Run only tests, which are required and at the end of large implementation, run the whole test suite. -## Terminology - -- Screen / Root Screen - a screen is a module that contains all the code for a single screen. It has its own ViewModel, UI and repos. It is responsible for rendering the state and delegating actions to the ViewModel. -- State - a state is an immutable data class that contains all the data needed to render the UI. It is returned by the ViewModel and rendered by the UI. -- Action - an action is a user interaction that is delegated to the ViewModel. It can be a button click, a gesture, a keyboard shortcut, etc. -- User intent - a user intent is a high-level action that the user wants to perform. It is translated into one or more actions that are delegated to the ViewModel. Naming is based on user intent, not technical terms. For example, "Open App" is a user intent, while "LaunchActivity" is a technical term. Contract between layers can be merged with technical term. -- Contract - defines communication between layers. Techncial terms can be used for these classes too. -- Repo - a repo is a class that is responsible for fetching data from a data source. It can be a local database, a remote API, or any other data source. It is used by the ViewModel to fetch data and return it as a state. -- Usecase - a usecase is a class that contains business logic and orchestrates data from multiple repos. It is used by the ViewModel to perform complex actions and return a state. It equals to a user intention to do something. +## Glossary + +### Recording domain + +- Recording - the activity of capturing data. +- Recording session - one start-to-stop recording operation. It may run on the phone alone or on both phone and watch. +- Recording setup - editable choices made before recording. +- Recording request - the immutable command created from the setup when recording starts. +- Recording source - an independently managed data producer, such as sensors or GPS. One source may create several files. +- Paired recording - one session coordinated across phone and watch. +- Measurement - one device's stored output for a recording session. A metadata-only measurement is valid. +- Measurement file - one stored data stream within a measurement. +- Measurement metadata - facts about the whole measurement, not a user-visible recording source. +- Sample - one timestamped observation, normally stored as one row. +- Recording archive - the selected phone directory containing phone and synced watch measurements. +- Sync - copying watch measurements to the phone archive without merging or removing the watch originals. +- Sensor - a sensor exposed by the phone or watch. Sensor type names its kind, such as accelerometer. +- Use phone and watch in domain code. Reserve Android and Wear OS for platform code. + +### MVI + +- Screen - stateless UI that renders state and sends intents. +- Intent - something the UI asks the ViewModel to handle. Do not use Action as a synonym. +- State - immutable data needed to render a screen. +- Effect - a one-time operation outside state rendering, such as navigation or requesting permission. +- ViewModel - owns state, handles intents, and coordinates domain operations. +- Reducer - a pure state transition. +- Contract - the state, intents, effects, and reducer used by a screen. +- Repo - a boundary for reading or changing stored data or platform state. +- Use case - one named domain operation or user goal. ## People @@ -58,4 +79,4 @@ You are agent. Me as user I am programmer, and we are building this project in collaboration. Explain your ideas, ask questions, give suggestions and think about possible architectural improvements based on the current state of the project. Review the code and give feedback. -Be straight and go to the point. \ No newline at end of file +Be straight and go to the point. diff --git a/CONTEXT.md b/CONTEXT.md index bd96788..18c668b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,14 +1,14 @@ # Recording archive -The recording archive contains measurements created on the phone or copied from Wear OS. +The recording archive is the selected phone directory containing phone measurements and synced watch measurements. ## Language **Measurement** -One recording session stored as a named folder in the selected recording directory. +One device's stored output for a recording session. A phone and watch can each create a measurement for the same paired recording. A metadata-only measurement is valid. -_Avoid:_ Recording file, sensor measurement +_Avoid:_ Recording session, recording file, sensor measurement **Measurement file** @@ -21,3 +21,45 @@ _Avoid:_ Measurement Session-level facts stored with a measurement, including its start time, notes, alarms, duration, and sensor ranges. _Avoid:_ Sensor metadata + +**Recording session** + +One start-to-stop recording operation. It can run on the phone alone or on both phone and watch. + +_Avoid:_ Measurement session + +**Recording setup** + +The editable choices made before a recording starts. + +_Avoid:_ Measurement setup, recording configuration + +**Recording request** + +The immutable command created from the recording setup when recording starts. + +_Avoid:_ Recording plan, measurement request, launch configuration + +**Recording source** + +An independently managed data producer, such as sensors or GPS. One recording source can create several measurement files. + +_Avoid:_ Recording device + +**Paired recording** + +One recording session coordinated across the phone and watch. + +_Avoid:_ watch recording + +**Sample** + +One timestamped observation, normally stored as one row in a measurement file. + +_Avoid:_ Sensor measurement + +**Sync** + +Copying watch measurements to the phone recording archive without merging them or removing the watch originals. + +_Avoid:_ Merge, move diff --git a/README.md b/README.md index 61c7505..bbda53d 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ This is the hard-cut Android 17 generation of the project. It does not retain th ## Screenshots -| Sensor selection | Measurement setup | +| Sensor selection | Recording setup | |:---:|:---:| -| SensorBox source selection | SensorBox measurement setup | +| SensorBox source selection | SensorBox recording setup | | Wear dashboard | Wear live-sensor picker | |:---:|:---:| @@ -20,11 +20,11 @@ This is the hard-cut Android 17 generation of the project. It does not retain th |:---:|:---:|:---:| | SensorBox welcome introduction | SensorBox local-data introduction | SensorBox privacy and terms introduction | -| Android lifecycle | Battery optimization | Recording folder | +| Android lifecycle | Battery optimization | Recording archive | |:---:|:---:|:---:| | SensorBox Android lifecycle introduction | SensorBox battery optimization introduction | SensorBox recording-folder introduction | -The introduction uses tintable vector illustrations that follow the app theme. Privacy Policy, Terms of Use, battery optimization, and folder selection are live native actions. Folder selection remains mandatory before setup can finish. +The introduction uses tintable vector illustrations that follow the app theme. Privacy Policy, Terms of Use, battery optimization, and recording-archive selection use native Android screens. A recording archive remains mandatory before setup can finish. All screenshots above come from deterministic Compose preview fixtures. Refresh the complete gallery on the host without an emulator or connected device: @@ -36,11 +36,11 @@ All screenshots above come from deterministic Compose preview fixtures. Refresh - Record available phone or watch sensors at Android sampling periods. - Record foreground GPS samples alongside sensor data. -- Run measurement work in an explicit foreground service with health/location service types. +- Run recording work in an explicit foreground service with health/location service types. - Stop safely from the app, watch, notification, low-battery policy, or a paired-device command. - Preview a live watch sensor with a Compose-native chart. - Stream watch recordings to the phone with the Wear OS Channel API. -- Store phone recordings in a user-selected Storage Access Framework folder. +- Store phone measurements in a user-selected recording archive through the Storage Access Framework. - Follow system/dynamic color with light, dark, and custom fallback palettes. ## Architecture @@ -96,14 +96,14 @@ Tests use Given/When/Then naming, reusable state/repository fixtures, coroutine ## Emulator integration tests -The phone recording tests start the real foreground measurement service, read the device sensors, control test GPS and battery state from Kotlin, and verify the generated files. Run the class directly from Android Studio or with Gradle: +The phone recording tests start the real foreground recording service, read the device sensors, control test GPS and battery state from Kotlin, and verify the generated files. Run the class directly from Android Studio or with Gradle: ```shell ANDROID_SERIAL=emulator-5554 ./gradlew :app:connectedDebugAndroidTest \ "-Pandroid.testInstrumentationRunnerArguments.class=com.tomasrepcik.sensorbox.emulator.PhoneSensorRecordingEmulatorTest" ``` -Standalone Wear recording tests use the watch sensors and control test GPS and battery state from Kotlin. They do not require a paired phone: +Standalone watch recording tests use the watch sensors and control test GPS and battery state from Kotlin. They do not require a paired phone: ```shell ANDROID_SERIAL=emulator-5554 ./gradlew :wear:connectedDebugAndroidTest \ @@ -116,7 +116,7 @@ The paired sync matrix sends CSV, JSON, text, empty, Unicode, overwrite, duplica ANDROID_HOME="$HOME/Library/Android/sdk" tools/emulator/run_wear_sync_test.sh ``` -The runner detects one phone and one watch automatically; `PHONE_SERIAL` and `WEAR_SERIAL` remain available when several devices are connected. It builds and installs once, refreshes the ADB bridge after installation, and launches each scenario on both devices. When an emulator transport exposes its paired node but does not propagate static capabilities, the instrumentation-only repository falls back to that connected node; file transfer still uses the production Channel client and receiver. Received files use app-internal storage only in debuggable builds; release builds continue to require the user-selected Storage Access Framework directory. +The runner detects one phone and one watch automatically; `PHONE_SERIAL` and `WEAR_SERIAL` remain available when several devices are connected. It builds and installs once, refreshes the ADB bridge after installation, and launches each scenario on both devices. When an emulator transport exposes its paired node but does not propagate static capabilities, the instrumentation-only repository falls back to that connected node; file transfer still uses the production Channel client and receiver. Received files use app-internal storage only in debuggable builds; release builds continue to require the user-selected recording archive. No Firebase project, Maps key, secrets file, or external storage permission is required. @@ -128,7 +128,7 @@ The former Flipper, AppIntro, Material Dialogs, NumberPicker, Android About Page ## Privacy -Measurements are initiated by the user, represented by an ongoing foreground-service notification, and written locally. SensorBox does not upload measurement data or include analytics/crash-reporting SDKs. +Recordings are started by the user and remain visible through a foreground-service notification. SensorBox writes each device's measurement locally and does not include upload, analytics, or crash-reporting SDKs. ## License diff --git a/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/files/GooglePlayWearFileTransferClient.kt b/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/files/GooglePlayWearFileTransferClient.kt index 51d06cb..e4d1a14 100644 --- a/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/files/GooglePlayWearFileTransferClient.kt +++ b/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/files/GooglePlayWearFileTransferClient.kt @@ -50,7 +50,7 @@ class GooglePlayWearFileTransferClient @Inject constructor(@ApplicationContext c is AppResult.Success -> opened.value.use(consume) } val close = closeChannel(channel) - listOf(transfer, close).combineAppResults(AppErrorCode.CONNECTIVITY, "Receive Wear file") + listOf(transfer, close).combineAppResults(AppErrorCode.CONNECTIVITY, "Receive watch file") } private suspend fun openChannel(nodeId: String, path: String): AppResult = diff --git a/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommand.kt b/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommand.kt index 6789ec0..8da1224 100644 --- a/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommand.kt +++ b/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommand.kt @@ -1,6 +1,7 @@ package com.tomasrepcik.sensorbox.wearoslib.protocol import com.tomasrepcik.sensorbox.core.error.AppErrorCode +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -26,7 +27,8 @@ sealed interface WearCommand { @Serializable data class RecordingResult( val sessionId: String, - val action: WearRecordingAction, + @SerialName("action") + val operation: WearRecordingOperation, val outcome: WearRecordingOutcome, @Serializable(with = AppErrorCodeNameSerializer::class) val errorCode: AppErrorCode? = null, @@ -46,7 +48,7 @@ data class WearRecordingRequest( ) @Serializable -enum class WearRecordingAction { +enum class WearRecordingOperation { START, STOP, } diff --git a/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodec.kt b/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodec.kt index 5a23440..e429f75 100644 --- a/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodec.kt +++ b/WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodec.kt @@ -57,6 +57,7 @@ internal object AppErrorCodeNameSerializer : KSerializer { override fun deserialize(decoder: Decoder): AppErrorCode { val name = decoder.decodeString() + if (name == "MEASUREMENT") return AppErrorCode.RECORDING return AppErrorCode.entries.firstOrNull { it.name == name } ?: error("Unknown application error code") } diff --git a/WearOsLib/src/test/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodecTest.kt b/WearOsLib/src/test/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodecTest.kt index 496b7dc..9033127 100644 --- a/WearOsLib/src/test/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodecTest.kt +++ b/WearOsLib/src/test/java/com/tomasrepcik/sensorbox/wearoslib/protocol/WearCommandCodecTest.kt @@ -40,16 +40,16 @@ class WearCommandCodecTest { WearCommand.StopRecording("session-123", WearStopReason.LOW_BATTERY), WearCommand.RecordingResult( sessionId = "session-123", - action = WearRecordingAction.START, + operation = WearRecordingOperation.START, outcome = WearRecordingOutcome.SUCCEEDED, ), WearCommand.RecordingResult( sessionId = "session-123", - action = WearRecordingAction.STOP, + operation = WearRecordingOperation.STOP, outcome = WearRecordingOutcome.FAILED, - errorCode = AppErrorCode.MEASUREMENT, - errorOperation = "Stop Wear recording", - errorMessage = "Wear recording service could not stop", + errorCode = AppErrorCode.RECORDING, + errorOperation = "Stop watch recording", + errorMessage = "watch recording service could not stop", errorContext = mapOf("serviceState" to "stopping"), failureCount = 2, ), @@ -79,9 +79,9 @@ class WearCommandCodecTest { fun `Given a successful result with an error When encoded Then it is rejected`() { val invalid = WearCommand.RecordingResult( sessionId = "session-123", - action = WearRecordingAction.START, + operation = WearRecordingOperation.START, outcome = WearRecordingOutcome.SUCCEEDED, - errorCode = AppErrorCode.MEASUREMENT, + errorCode = AppErrorCode.RECORDING, errorOperation = "Unexpected failure", errorMessage = "A successful result cannot contain an error", ) diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneSensorRecordingEmulatorTest.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneSensorRecordingEmulatorTest.kt index 1e2c19c..d930c3c 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneSensorRecordingEmulatorTest.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneSensorRecordingEmulatorTest.kt @@ -142,7 +142,7 @@ class PhoneSensorRecordingEmulatorTest { } @Test - fun givenSignificantMotionWhenStoppedThenThePreparedFileCloses() { + fun givenSignificantMotionRecordingWhenStoppedThenThePreparedFileCloses() { fixture.start( RecordingScenario( name = "PHONE_SIGNIFICANT_MOTION_FAILURE_TEST", diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneWearDiscoveryEmulatorTest.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneWearDiscoveryEmulatorTest.kt index ef34dc3..19d1526 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneWearDiscoveryEmulatorTest.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/PhoneWearDiscoveryEmulatorTest.kt @@ -12,7 +12,7 @@ import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_MESSAGE_PATH import com.tomasrepcik.sensorbox.wearoslib.connectivity.GooglePlayWearConnectionRepository import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommandCodec -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOutcome import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingRequest import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason @@ -102,7 +102,7 @@ class PhoneWearDiscoveryEmulatorTest { ).getOrThrow() val result = awaitCommand(commands) { - it.sessionId == sessionId && it.action == WearRecordingAction.START + it.sessionId == sessionId && it.operation == WearRecordingOperation.START } assertEquals(WearRecordingOutcome.SUCCEEDED, result.outcome) } finally { @@ -147,10 +147,10 @@ class PhoneWearDiscoveryEmulatorTest { ).getOrThrow() val result = awaitCommand(commands) { - it.sessionId == sessionId && it.action == WearRecordingAction.START + it.sessionId == sessionId && it.operation == WearRecordingOperation.START } assertEquals(WearRecordingOutcome.FAILED, result.outcome) - assertEquals("Validate Wear recording permissions", result.errorOperation) + assertEquals("Validate watch recording permissions", result.errorOperation) assertEquals("Wear OS is missing required recording permissions", result.errorMessage) assertTrue( result.errorContext["missingPermissions"] diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/RecordingEmulatorFixture.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/RecordingEmulatorFixture.kt index 07c7ea2..fea4258 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/RecordingEmulatorFixture.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/RecordingEmulatorFixture.kt @@ -10,9 +10,9 @@ import android.os.ParcelFileDescriptor import androidx.core.content.ContextCompat import androidx.test.platform.app.InstrumentationRegistry import com.tomasrepcik.sensorbox.core.time.SystemEpochClock -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementIntentFactory -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest -import com.tomasrepcik.sensorbox.sensorservices.services.MeasurementService +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingIntentFactory +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest +import com.tomasrepcik.sensorbox.sensorservices.services.RecordingService import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -84,7 +84,7 @@ internal class RecordingEmulatorFixture(private val context: Context) { if (!scenario.useInternalStorage) clearDocumentStoragePermissions() if (scenario.includesGps) prepareMockGps() val scheduledStartMillis = SystemEpochClock.nowMillis() - val request = MeasurementLaunchRequest( + val request = RecordingRequest( sessionId = scenario.sessionId, folderName = scenario.name, useInternalStorage = scenario.useInternalStorage, @@ -102,7 +102,7 @@ internal class RecordingEmulatorFixture(private val context: Context) { activityRecognitionPeriodSeconds = scenario.activityRecognitionPeriodSeconds, significantMotion = scenario.significantMotion, ) - val intent = MeasurementIntentFactory(context, SystemEpochClock).create(request) + val intent = RecordingIntentFactory(context, SystemEpochClock).create(request) assertIntentMatches(intent, request) ContextCompat.startForegroundService(context, intent) val expectedRecordedTypes = scenario.sensors.mapTo(mutableSetOf(), RecordedSensor::type).apply { @@ -116,40 +116,40 @@ internal class RecordingEmulatorFixture(private val context: Context) { return ActiveRecording(this, scenario, scheduledStartMillis, expectedRecordedTypes) } - private fun assertIntentMatches(intent: Intent, request: MeasurementLaunchRequest) { - assertEquals(request.sessionId, intent.getStringExtra(MeasurementService.SESSION_ID)) - assertEquals(request.folderName, intent.getStringExtra(MeasurementService.FOLDER_NAME)) - assertEquals(request.useInternalStorage, intent.getBooleanExtra(MeasurementService.INTERNAL_STORAGE, false)) - val sensorIds = intent.getIntArrayExtra(MeasurementService.ANDROID_SENSORS) ?: intArrayOf() + private fun assertIntentMatches(intent: Intent, request: RecordingRequest) { + assertEquals(request.sessionId, intent.getStringExtra(RecordingService.SESSION_ID)) + assertEquals(request.folderName, intent.getStringExtra(RecordingService.FOLDER_NAME)) + assertEquals(request.useInternalStorage, intent.getBooleanExtra(RecordingService.INTERNAL_STORAGE, false)) + val sensorIds = intent.getIntArrayExtra(RecordingService.ANDROID_SENSORS) ?: intArrayOf() assertEquals(request.sensorIds, sensorIds.toSet()) assertEquals( request.sensorSamplingPeriod, - intent.getIntExtra(MeasurementService.ANDROID_SENSORS_SPEED, Int.MIN_VALUE), + intent.getIntExtra(RecordingService.ANDROID_SENSORS_SPEED, Int.MIN_VALUE), ) - assertEquals(request.includesGps, intent.getBooleanExtra(MeasurementService.GPS, false)) - assertEquals(request.stopOnLowBattery, intent.getBooleanExtra(MeasurementService.STOP_ON_LOW_BATTERY, false)) - assertEquals(request.useWakeLock, intent.getBooleanExtra(MeasurementService.USE_WAKE_LOCK, false)) - assertEquals(request.gpsIntervalSeconds, intent.getIntExtra(MeasurementService.GPS_INTERVAL_SECONDS, -1)) - assertEquals(request.gpsMinDistanceMeters, intent.getIntExtra(MeasurementService.GPS_DISTANCE_METERS, -1)) - assertEquals(request.durationMillis, intent.getLongExtra(MeasurementService.DURATION_MILLIS, -1)) - assertEquals(request.notes, intent.getStringArrayListExtra(MeasurementService.NOTES).orEmpty()) + assertEquals(request.includesGps, intent.getBooleanExtra(RecordingService.GPS, false)) + assertEquals(request.stopOnLowBattery, intent.getBooleanExtra(RecordingService.STOP_ON_LOW_BATTERY, false)) + assertEquals(request.useWakeLock, intent.getBooleanExtra(RecordingService.USE_WAKE_LOCK, false)) + assertEquals(request.gpsIntervalSeconds, intent.getIntExtra(RecordingService.GPS_INTERVAL_SECONDS, -1)) + assertEquals(request.gpsMinDistanceMeters, intent.getIntExtra(RecordingService.GPS_DISTANCE_METERS, -1)) + assertEquals(request.durationMillis, intent.getLongExtra(RecordingService.DURATION_MILLIS, -1)) + assertEquals(request.notes, intent.getStringArrayListExtra(RecordingService.NOTES).orEmpty()) assertEquals( request.alarmOffsetsSeconds, - (intent.getIntArrayExtra(MeasurementService.ALARM_OFFSETS_SECONDS) ?: intArrayOf()).toList(), + (intent.getIntArrayExtra(RecordingService.ALARM_OFFSETS_SECONDS) ?: intArrayOf()).toList(), ) assertEquals( request.activityRecognition, - intent.getBooleanExtra(MeasurementService.ACTIVITY_RECOGNITION, false), + intent.getBooleanExtra(RecordingService.ACTIVITY_RECOGNITION, false), ) assertEquals( request.activityRecognitionPeriodSeconds, - intent.getIntExtra(MeasurementService.ACTIVITY_RECOGNITION_PERIOD_SECONDS, -1), + intent.getIntExtra(RecordingService.ACTIVITY_RECOGNITION_PERIOD_SECONDS, -1), ) - assertEquals(request.significantMotion, intent.getBooleanExtra(MeasurementService.SIGNIFICANT_MOTION, false)) + assertEquals(request.significantMotion, intent.getBooleanExtra(RecordingService.SIGNIFICANT_MOTION, false)) } fun stopAnyRecording() { - context.startService(serviceIntent(MeasurementService.ACTION_STOP)) + context.startService(serviceIntent(RecordingService.ACTION_STOP_RECORDING)) } private fun grantRuntimePermissions() { @@ -210,9 +210,9 @@ internal class RecordingEmulatorFixture(private val context: Context) { .use { it.readText() } private fun annotate(text: String) { - context.startService(serviceIntent(MeasurementService.ACTION_ANNOTATE).apply { - putExtra(MeasurementService.ANNOTATION_TIME, SystemEpochClock.nowMillis()) - putExtra(MeasurementService.ANNOTATION_TEXT, text) + context.startService(serviceIntent(RecordingService.ACTION_ANNOTATE).apply { + putExtra(RecordingService.ANNOTATION_TIME, SystemEpochClock.nowMillis()) + putExtra(RecordingService.ANNOTATION_TEXT, text) }) } @@ -263,7 +263,7 @@ internal class RecordingEmulatorFixture(private val context: Context) { private fun measurementDirectory(measurementName: String): File = File(context.filesDir, "SensorBox/$measurementName") - private fun serviceIntent(actionName: String): Intent = Intent(context, MeasurementService::class.java).apply { + private fun serviceIntent(actionName: String): Intent = Intent(context, RecordingService::class.java).apply { action = actionName } diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenRobot.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenRobot.kt index 20d81b3..e7d21ac 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenRobot.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenRobot.kt @@ -17,10 +17,10 @@ class OnboardingScreenRobot(private val rule: ComposeContentTestRule) { var lastIntent: OnboardingIntent? = null private set - fun givenInteractiveOnboarding(page: Int = 0, storagePath: String? = null) = apply { + fun givenInteractiveOnboarding(page: Int = 0, recordingArchivePath: String? = null) = apply { rule.setContent { var state by remember { - mutableStateOf(OnboardingState(page = page, storagePath = storagePath)) + mutableStateOf(OnboardingState(page = page, recordingArchivePath = recordingArchivePath)) } SensorBoxTheme { OnboardingScreen(state) { intent -> @@ -57,7 +57,7 @@ class OnboardingScreenRobot(private val rule: ComposeContentTestRule) { } fun whenChooseFolderIsTapped() = apply { - rule.onNodeWithText("Choose folder").performClick() + rule.onNodeWithText("Choose archive").performClick() } fun whenFinishIsTapped() = apply { diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenTest.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenTest.kt index daaf4c2..fe75630 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenTest.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreenTest.kt @@ -22,16 +22,16 @@ class OnboardingScreenTest { robot.whenNextIsTapped().thenPageIsVisible("Android may pause recordings").whenNextIsTapped() robot.thenPageIsVisible("Allow reliable background work").whenBatterySettingsIsTapped() assertEquals(OnboardingIntent.RequestBatteryOptimizationExemption, robot.lastIntent) - robot.whenNextIsTapped().thenPageIsVisible("Choose a recording folder") + robot.whenNextIsTapped().thenPageIsVisible("Choose a recording archive") robot.thenFinishIsDisabled().whenChooseFolderIsTapped() - assertEquals(OnboardingIntent.ChooseStorage, robot.lastIntent) + assertEquals(OnboardingIntent.ChooseRecordingArchive, robot.lastIntent) } @Test fun givenSelectedFolderWhenFinishingThenCompleteIntroductionIntentIsSent() { val robot = OnboardingScreenRobot(composeRule).givenInteractiveOnboarding( page = 5, - storagePath = "Documents/SensorBox", + recordingArchivePath = "Documents/SensorBox", ) robot.thenFinishIsEnabled().whenFinishIsTapped() diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenRobot.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenRobot.kt index 35162a9..53c1782 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenRobot.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenRobot.kt @@ -13,7 +13,7 @@ class RecordScreenRobot(private val rule: ComposeContentTestRule) { fun givenRecordScreen( selected: Boolean = false, gpsSelected: Boolean = false, - wearConnected: Boolean = false, + watchConnected: Boolean = false, onIntent: (RecordingIntent) -> Unit = {}, ) = apply { rule.setContent { @@ -23,13 +23,13 @@ class RecordScreenRobot(private val rule: ComposeContentTestRule) { sensors = listOf(SensorDescriptor(1, "Accelerometer", "Fixture")), selectedSensorIds = if (selected) setOf(1) else emptySet(), includesGps = gpsSelected, - isWearConnected = wearConnected, - wearSensors = if (wearConnected) { + isWatchConnected = watchConnected, + watchSensors = if (watchConnected) { listOf(SensorDescriptor(1, "Wear Accelerometer", "Fixture")) } else { emptyList() }, - storagePath = "Fixture/SensorBox", + recordingArchivePath = "Fixture/SensorBox", ), onIntent = onIntent, ) @@ -58,7 +58,7 @@ class RecordScreenRobot(private val rule: ComposeContentTestRule) { rule.onNodeWithText("Continue").performClick() } - fun thenRecordingActionIsVisible() = apply { + fun thenRecordingButtonIsVisible() = apply { rule.onNodeWithText("Continue").assertIsDisplayed() } } diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenTest.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenTest.kt index 4af5b7b..75c3bd3 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenTest.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreenTest.kt @@ -14,7 +14,7 @@ class RecordScreenTest { var actualIntent: RecordingIntent? = null RecordScreenRobot(composeRule) .givenRecordScreen { actualIntent = it } - .thenRecordingActionIsVisible() + .thenRecordingButtonIsVisible() .whenAccelerometerIsTapped() composeRule.runOnIdle { @@ -42,7 +42,7 @@ class RecordScreenTest { .whenContinueIsTapped() composeRule.runOnIdle { - assertEquals(RecordingIntent.OpenMeasurementSetup, actualIntent) + assertEquals(RecordingIntent.OpenRecordingSetup, actualIntent) } } @@ -62,12 +62,12 @@ class RecordScreenTest { fun givenWearSensorWhenInfoIsTappedThenWearDetailsIntentIsSent() { var actualIntent: RecordingIntent? = null RecordScreenRobot(composeRule) - .givenRecordScreen(wearConnected = true) { actualIntent = it } + .givenRecordScreen(watchConnected = true) { actualIntent = it } .whenWearAccelerometerInfoIsTapped() composeRule.runOnIdle { assertEquals( - RecordingIntent.OpenSensorDetails(1, RecordingSensorSource.WEAR), + RecordingIntent.OpenSensorDetails(1, RecordingDevice.WATCH), actualIntent, ) } @@ -81,7 +81,7 @@ class RecordScreenTest { .whenContinueIsTapped() composeRule.runOnIdle { - assertEquals(RecordingIntent.OpenMeasurementSetup, actualIntent) + assertEquals(RecordingIntent.OpenRecordingSetup, actualIntent) } } } diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreenRobot.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreenRobot.kt similarity index 66% rename from app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreenRobot.kt rename to app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreenRobot.kt index 5631e72..25df53d 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreenRobot.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreenRobot.kt @@ -7,15 +7,15 @@ import androidx.compose.ui.test.performClick import com.tomasrepcik.sensorbox.domain.sensors.SensorDescriptor import com.tomasrepcik.sensorbox.ui.theme.SensorBoxTheme -class MeasurementSetupScreenRobot(private val rule: ComposeContentTestRule) { - fun givenMeasurementSetup(onIntent: (RecordingIntent) -> Unit = {}) = apply { +class RecordingSetupScreenRobot(private val rule: ComposeContentTestRule) { + fun givenRecordingSetup(onIntent: (RecordingIntent) -> Unit = {}) = apply { rule.setContent { SensorBoxTheme { - MeasurementSetupScreen( + RecordingSetupScreen( state = RecordingState( sensors = listOf(SensorDescriptor(1, "Accelerometer", "Fixture")), selectedSensorIds = setOf(1), - storagePath = "Fixture/SensorBox", + recordingArchivePath = "Fixture/SensorBox", ), onIntent = onIntent, ) @@ -23,12 +23,12 @@ class MeasurementSetupScreenRobot(private val rule: ComposeContentTestRule) { } } - fun whenStartMeasurementIsTapped() = apply { - rule.onNodeWithText("Start measurement").performClick() + fun whenStartRecordingIsTapped() = apply { + rule.onNodeWithText("Start recording").performClick() } fun thenFolderAndSettingsAreVisible() = apply { - rule.onNodeWithText("Recording folder").assertIsDisplayed() + rule.onNodeWithText("Recording archive").assertIsDisplayed() rule.onNodeWithText("Sensor sampling").assertIsDisplayed() } } diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreenTest.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreenTest.kt similarity index 73% rename from app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreenTest.kt rename to app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreenTest.kt index 0ad3245..fb284e8 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreenTest.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreenTest.kt @@ -8,20 +8,20 @@ import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test -class MeasurementSetupScreenTest { +class RecordingSetupScreenTest { @get:Rule val composeRule = createComposeRule() @Test - fun givenConfiguredSetupWhenStartIsTappedThenMeasurementIntentIsSent() { + fun givenConfiguredSetupWhenStartIsTappedThenRecordingRequestIsSent() { var actualIntent: RecordingIntent? = null - MeasurementSetupScreenRobot(composeRule) - .givenMeasurementSetup { actualIntent = it } + RecordingSetupScreenRobot(composeRule) + .givenRecordingSetup { actualIntent = it } .thenFolderAndSettingsAreVisible() - .whenStartMeasurementIsTapped() + .whenStartRecordingIsTapped() composeRule.runOnIdle { - assertEquals(RecordingIntent.StartMeasurement, actualIntent) + assertEquals(RecordingIntent.StartRecording, actualIntent) } } @@ -29,7 +29,7 @@ class MeasurementSetupScreenTest { fun givenWearPermissionFailureThenActionableWatchGuidanceIsDisplayed() { composeRule.setContent { SensorBoxTheme { - RecordingMessageText(RecordingMessage.WEAR_PERMISSION_REQUIRED) + RecordingMessageText(RecordingMessage.WATCH_PERMISSION_REQUIRED) } } @@ -43,10 +43,10 @@ class MeasurementSetupScreenTest { fun givenRecordingIsStartingThenProgressLabelIsDisplayed() { composeRule.setContent { SensorBoxTheme { - MeasurementSetupScreen( + RecordingSetupScreen( state = RecordingState( selectedSensorIds = setOf(1), - storagePath = "Fixture/SensorBox", + recordingArchivePath = "Fixture/SensorBox", isStarting = true, ), onIntent = { }, diff --git a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreenTest.kt b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreenTest.kt index 39e05cd..38477d7 100644 --- a/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreenTest.kt +++ b/app/src/androidTest/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreenTest.kt @@ -22,7 +22,7 @@ class SensorDetailsScreenTest { SensorBoxTheme { SensorDetailsScreen( state = RecordingState( - wearSensors = listOf( + watchSensors = listOf( SensorDescriptor( type = 1, name = "Wear Accelerometer", @@ -39,7 +39,7 @@ class SensorDetailsScreenTest { ), ), detailsSensorType = 1, - detailsSensorSource = RecordingSensorSource.WEAR, + detailsDevice = RecordingDevice.WATCH, ), onBack = {}, onPreview = {}, diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/MsgListener.kt b/app/src/main/java/com/tomasrepcik/sensorbox/MsgListener.kt index e04b661..a13409b 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/MsgListener.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/MsgListener.kt @@ -3,8 +3,8 @@ package com.tomasrepcik.sensorbox import com.google.android.gms.wearable.ChannelClient import com.google.android.gms.wearable.MessageEvent import com.google.android.gms.wearable.WearableListenerService -import com.tomasrepcik.sensorbox.domain.paired.PhoneWearMessageDispatcher -import com.tomasrepcik.sensorbox.domain.sync.ReceiveWearFileUseCase +import com.tomasrepcik.sensorbox.domain.paired.PhoneWatchMessageDispatcher +import com.tomasrepcik.sensorbox.domain.sync.ReceiveWatchFileUseCase import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -16,10 +16,10 @@ import javax.inject.Inject @AndroidEntryPoint class MsgListener : WearableListenerService() { @Inject - lateinit var messageDispatcher: PhoneWearMessageDispatcher + lateinit var messageDispatcher: PhoneWatchMessageDispatcher @Inject - lateinit var receiveWearFile: ReceiveWearFileUseCase + lateinit var receiveWearFile: ReceiveWatchFileUseCase private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/activities/MainActivity.kt b/app/src/main/java/com/tomasrepcik/sensorbox/activities/MainActivity.kt index 29c8c65..f450cb9 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/activities/MainActivity.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/activities/MainActivity.kt @@ -46,11 +46,11 @@ class MainActivity : ComponentActivity() { private val recordingViewModel: RecordingViewModel by viewModels() private val measurementBrowserViewModel: MeasurementBrowserViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels() - private var storageRequestOwner = StorageRequestOwner.RECORDING + private var recordingArchiveRequestOwner = RecordingArchiveRequestOwner.RECORDING private val directoryPicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - when (storageRequestOwner) { - StorageRequestOwner.ONBOARDING -> onboardingViewModel.handleStorageResult(it.data) - StorageRequestOwner.RECORDING -> recordingViewModel.handleStorageResult(it.data) + when (recordingArchiveRequestOwner) { + RecordingArchiveRequestOwner.ONBOARDING -> onboardingViewModel.handleRecordingArchiveResult(it.data) + RecordingArchiveRequestOwner.RECORDING -> recordingViewModel.handleRecordingArchiveResult(it.data) } } private val permissionRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { @@ -100,7 +100,7 @@ class MainActivity : ComponentActivity() { private fun handleOnboardingEffect(effect: OnboardingEffect) { when (effect) { - OnboardingEffect.PickStorageDirectory -> openStoragePicker(StorageRequestOwner.ONBOARDING) + OnboardingEffect.PickRecordingArchive -> openRecordingArchivePicker(RecordingArchiveRequestOwner.ONBOARDING) OnboardingEffect.OpenPrivacyPolicy -> openWebPage(getString(R.string.link_privacy_policy)) OnboardingEffect.OpenTermsOfUse -> openWebPage(getString(R.string.link_terms)) OnboardingEffect.RequestBatteryOptimizationExemption -> requestBatteryOptimizationExemption() @@ -110,7 +110,7 @@ class MainActivity : ComponentActivity() { private fun handleRecordingEffect(effect: RecordingEffect) { when (effect) { - RecordingEffect.PickStorageDirectory -> openStoragePicker(StorageRequestOwner.RECORDING) + RecordingEffect.PickRecordingArchive -> openRecordingArchivePicker(RecordingArchiveRequestOwner.RECORDING) is RecordingEffect.Navigate -> mainViewModel.navigate(effect.route) @@ -127,7 +127,7 @@ class MainActivity : ComponentActivity() { SettingsEffect.ShareDiagnosticsFile -> shareDiagnosticsFile() is SettingsEffect.CopyDiagnosticsText -> copyDiagnostics(effect.text) SettingsEffect.DiagnosticsCleared -> showToast(R.string.diagnostics_cleared) - is SettingsEffect.DiagnosticsFailed -> showToast(R.string.diagnostics_action_failed) + is SettingsEffect.DiagnosticsFailed -> showToast(R.string.diagnostics_operation_failed) is SettingsEffect.Navigate -> mainViewModel.navigate(effect.route) } } @@ -138,10 +138,10 @@ class MainActivity : ComponentActivity() { } } - private fun openStoragePicker(owner: StorageRequestOwner) { - storageRequestOwner = owner - appResult(AppErrorCode.EXTERNAL_ACTION, "Open storage picker") { - directoryPicker.launch(storageIntent()) + private fun openRecordingArchivePicker(owner: RecordingArchiveRequestOwner) { + recordingArchiveRequestOwner = owner + appResult(AppErrorCode.EXTERNAL_ACTION, "Open recording archive picker") { + directoryPicker.launch(recordingArchiveIntent()) } } @@ -215,7 +215,7 @@ class MainActivity : ComponentActivity() { ) }.fold( onSuccess = { showToast(R.string.diagnostics_copied) }, - onFailure = { showToast(R.string.diagnostics_action_failed) }, + onFailure = { showToast(R.string.diagnostics_operation_failed) }, ) } @@ -223,13 +223,13 @@ class MainActivity : ComponentActivity() { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } - private fun storageIntent() = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + private fun recordingArchiveIntent() = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) } - private enum class StorageRequestOwner { + private enum class RecordingArchiveRequestOwner { ONBOARDING, RECORDING, } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/data/measurements/AndroidMeasurementRepository.kt b/app/src/main/java/com/tomasrepcik/sensorbox/data/measurements/AndroidMeasurementRepository.kt index fc2dab6..0476208 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/data/measurements/AndroidMeasurementRepository.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/data/measurements/AndroidMeasurementRepository.kt @@ -81,10 +81,10 @@ class AndroidMeasurementRepository @Inject constructor(@ApplicationContext priva val permission = context.contentResolver.persistedUriPermissions .filter { it.isReadPermission } .maxByOrNull { it.persistedTime } - ?: throw IllegalStateException("Recording directory is not configured") + ?: throw IllegalStateException("Recording archive is not configured") return DocumentFile.fromTreeUri(context, permission.uri) ?.takeIf(DocumentFile::isDirectory) - ?: throw IllegalStateException("Recording directory is unavailable") + ?: throw IllegalStateException("Recording archive is unavailable") } private fun measurementDirectory(measurementId: String): DocumentFile = selectedMeasurementsDirectory().listFiles() diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt b/app/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt index 6b5aee2..7e00246 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt @@ -1,15 +1,15 @@ package com.tomasrepcik.sensorbox.di import com.tomasrepcik.sensorbox.data.measurements.AndroidMeasurementRepository -import com.tomasrepcik.sensorbox.domain.measurement.AndroidPhoneRecordingController -import com.tomasrepcik.sensorbox.domain.measurement.DocumentStorageGateway -import com.tomasrepcik.sensorbox.domain.measurement.DocumentStorageUseCase -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementControlUseCase -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementPermissionUseCase -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementPermissionsUseCase -import com.tomasrepcik.sensorbox.domain.measurement.PhoneRecordingController -import com.tomasrepcik.sensorbox.domain.measurement.RecordingControlUseCase import com.tomasrepcik.sensorbox.domain.measurements.MeasurementRepository +import com.tomasrepcik.sensorbox.domain.recording.AndroidPhoneRecordingController +import com.tomasrepcik.sensorbox.domain.recording.DefaultRecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.DocumentRecordingArchiveRepository +import com.tomasrepcik.sensorbox.domain.recording.PhoneRecordingController +import com.tomasrepcik.sensorbox.domain.recording.RecordingArchiveRepository +import com.tomasrepcik.sensorbox.domain.recording.RecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingPermissionUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingPermissionsUseCase import com.tomasrepcik.sensorbox.domain.sensors.AvailableSensorsUseCase import com.tomasrepcik.sensorbox.domain.sensors.GetAvailableSensorsUseCase import com.tomasrepcik.sensorbox.presentation.main.AndroidElapsedRealtimeClock @@ -28,18 +28,20 @@ abstract class RecordingModule { ): PhoneRecordingController @Binds - abstract fun bindDocumentStorageGateway(implementation: DocumentStorageUseCase): DocumentStorageGateway + abstract fun bindRecordingArchiveRepository( + implementation: DocumentRecordingArchiveRepository, + ): RecordingArchiveRepository @Binds abstract fun bindAvailableSensorsUseCase(implementation: GetAvailableSensorsUseCase): AvailableSensorsUseCase @Binds - abstract fun bindMeasurementPermissionsUseCase( - implementation: MeasurementPermissionUseCase, - ): MeasurementPermissionsUseCase + abstract fun bindRecordingPermissionsUseCase( + implementation: RecordingPermissionUseCase, + ): RecordingPermissionsUseCase @Binds - abstract fun bindRecordingControlUseCase(implementation: MeasurementControlUseCase): RecordingControlUseCase + abstract fun bindRecordingControlUseCase(implementation: DefaultRecordingControlUseCase): RecordingControlUseCase @Binds abstract fun bindMeasurementRepository(implementation: AndroidMeasurementRepository): MeasurementRepository diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/DocumentStorageUseCase.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/DocumentStorageUseCase.kt deleted file mode 100644 index 7bcc99f..0000000 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/DocumentStorageUseCase.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tomasrepcik.sensorbox.domain.measurement - -import android.content.Intent -import com.tomasrepcik.sensorbox.core.error.AppResult -import com.tomasrepcik.sensorbox.core.storage.DocumentStorage -import javax.inject.Inject - -interface DocumentStorageGateway { - fun hasStorage(): AppResult - - fun displayPath(): AppResult - - fun persist(resultIntent: Intent): AppResult -} - -class DocumentStorageUseCase @Inject constructor(private val storage: DocumentStorage) : DocumentStorageGateway { - override fun hasStorage(): AppResult = storage.hasConfiguredDirectory() - - override fun displayPath(): AppResult = storage.displayPath() - - override fun persist(resultIntent: Intent): AppResult = storage.persistRootAccess(resultIntent) -} diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinator.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinator.kt index e502643..f6c4b40 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinator.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinator.kt @@ -6,14 +6,14 @@ import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.DiagnosticLogger import com.tomasrepcik.sensorbox.core.error.combineAppResults import com.tomasrepcik.sensorbox.core.error.toDiagnosticEvent -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementRequest -import com.tomasrepcik.sensorbox.domain.measurement.PhoneRecordingController +import com.tomasrepcik.sensorbox.domain.recording.PhoneRecordingController +import com.tomasrepcik.sensorbox.domain.recording.RecordingSetup import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_APP_CAPABILITY import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_MESSAGE_PATH import com.tomasrepcik.sensorbox.wearoslib.protocol.SendWearCommandUseCase import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommandCodec -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import com.tomasrepcik.sensorbox.wearoslib.protocol.recordingSessionId import kotlinx.coroutines.withTimeoutOrNull @@ -25,13 +25,13 @@ import javax.inject.Singleton class PairedRecordingCoordinator @Inject constructor( private val phoneRecording: PhoneRecordingController, private val sendCommand: SendWearCommandUseCase, - private val watchResults: WearRecordingResultInbox, + private val watchResults: WatchRecordingResultInbox, private val diagnosticLogger: DiagnosticLogger, ) { private var state: PairedRecordingState = PairedRecordingState.Idle @Suppress("ReturnCount") - suspend fun start(request: MeasurementRequest): AppResult { + suspend fun start(request: RecordingSetup): AppResult { if (state !is PairedRecordingState.Idle) return conflict("Start recording") val sessionId = UUID.randomUUID().toString() @@ -49,14 +49,14 @@ class PairedRecordingCoordinator @Inject constructor( } val watchStart = if (includesWatch) { - exchange(startedPhone.toWatchStartCommand(request), WearRecordingAction.START) + exchange(startedPhone.toWatchStartCommand(request), WearRecordingOperation.START) } else { AppResult.success(Unit) } if (state !== starting) return AppResult.success(Unit) if (watchStart is AppResult.Failure) { - if (watchStart.error.wasWearCommandSent()) { + if (watchStart.error.wasWatchCommandSent()) { record(watchStart.error) state = PairedRecordingState.Recording(sessionId, includesWatch) return AppResult.success(Unit) @@ -103,20 +103,20 @@ class PairedRecordingCoordinator @Inject constructor( val watchStop = stopWatch(sessionId, includesWatch, reason) state = PairedRecordingState.Idle return listOf(phoneStop, watchStop) - .combineAppResults(AppErrorCode.MEASUREMENT, "Stop phone and Wear recording") + .combineAppResults(AppErrorCode.RECORDING, "Stop phone and watch recording") .onFailure(::record) } private suspend fun stopWatch(sessionId: String, includesWatch: Boolean, reason: WearStopReason): AppResult = if (includesWatch) { - exchange(WearCommand.StopRecording(sessionId, reason), WearRecordingAction.STOP) + exchange(WearCommand.StopRecording(sessionId, reason), WearRecordingOperation.STOP) } else { AppResult.success(Unit) } - private suspend fun exchange(command: WearCommand, expectedAction: WearRecordingAction): AppResult { + private suspend fun exchange(command: WearCommand, expectedOperation: WearRecordingOperation): AppResult { val sessionId = checkNotNull(command.recordingSessionId()) - watchResults.clear(sessionId, expectedAction) + watchResults.clear(sessionId, expectedOperation) var lastSendError: AppError? = null var commandWasSent = false @@ -128,7 +128,7 @@ class PairedRecordingCoordinator @Inject constructor( commandWasSent = true lastSendError = null val result = withTimeoutOrNull(RESULT_TIMEOUT_MILLIS / ATTEMPT_COUNT) { - watchResults.await(sessionId, expectedAction) + watchResults.await(sessionId, expectedOperation) } if (result != null) return result.toAppResult(retryCount) } @@ -140,10 +140,10 @@ class PairedRecordingCoordinator @Inject constructor( return AppResult.failure( AppError( code = AppErrorCode.TIMEOUT, - operation = expectedAction.timeoutOperation(), - diagnosticMessage = "Wear $expectedAction result timed out", + operation = expectedOperation.timeoutOperation(), + diagnosticMessage = "watch $expectedOperation result timed out", context = mapOf( - "source" to "wear", + "source" to "watch", "sessionId" to sessionId, "retryCount" to RETRY_COUNT.toString(), "protocolVersion" to WearCommandCodec.PROTOCOL_VERSION.toString(), @@ -154,7 +154,7 @@ class PairedRecordingCoordinator @Inject constructor( ) } - private fun AppError.wasWearCommandSent(): Boolean = + private fun AppError.wasWatchCommandSent(): Boolean = code == AppErrorCode.TIMEOUT && context["commandWasSent"] == true.toString() private fun conflict(operation: String): AppResult = diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingModel.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingModel.kt index c5514a4..26a8e22 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingModel.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingModel.kt @@ -3,12 +3,12 @@ package com.tomasrepcik.sensorbox.domain.paired import com.tomasrepcik.sensorbox.core.error.AppError import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementRequest -import com.tomasrepcik.sensorbox.domain.measurement.StartedPhoneRecording -import com.tomasrepcik.sensorbox.domain.measurement.toWearRecordingRequest +import com.tomasrepcik.sensorbox.domain.recording.RecordingSetup +import com.tomasrepcik.sensorbox.domain.recording.StartedPhoneRecording +import com.tomasrepcik.sensorbox.domain.recording.toWatchRecordingRequest import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommandCodec -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOutcome internal sealed interface PairedRecordingState { @@ -30,11 +30,11 @@ internal fun PairedRecordingState.activeRecording(): ActivePairedRecording? = wh PairedRecordingState.Idle -> null } -internal fun MeasurementRequest.includesWatchRecording(): Boolean = wearSensorIds.isNotEmpty() || wearIncludesGps +internal fun RecordingSetup.includesWatchRecording(): Boolean = watchSensorIds.isNotEmpty() || watchIncludesGps -internal fun StartedPhoneRecording.toWatchStartCommand(request: MeasurementRequest) = WearCommand.StartRecording( +internal fun StartedPhoneRecording.toWatchStartCommand(request: RecordingSetup) = WearCommand.StartRecording( sessionId = sessionId, - request = toWearRecordingRequest(request), + request = toWatchRecordingRequest(request), ) internal fun WearCommand.RecordingResult.toAppResult(retryCount: Int): AppResult = @@ -44,10 +44,10 @@ internal fun WearCommand.RecordingResult.toAppResult(retryCount: Int): AppResult AppResult.failure( AppError( code = errorCode ?: AppErrorCode.UNKNOWN, - operation = errorOperation ?: "Handle Wear $action result", - diagnosticMessage = errorMessage ?: "Wear $action failed", + operation = errorOperation ?: "Handle watch $operation result", + diagnosticMessage = errorMessage ?: "Watch $operation failed", context = errorContext + mapOf( - "source" to "wear", + "source" to "watch", "sessionId" to sessionId, "retryCount" to retryCount.toString(), "failureCount" to failureCount.toString(), @@ -57,4 +57,4 @@ internal fun WearCommand.RecordingResult.toAppResult(retryCount: Int): AppResult ) } -internal fun WearRecordingAction.timeoutOperation(): String = "Await Wear $name result" +internal fun WearRecordingOperation.timeoutOperation(): String = "Await watch $name result" diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneRecordingSessionObserver.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneRecordingSessionObserver.kt index baa3460..4bc1be8 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneRecordingSessionObserver.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneRecordingSessionObserver.kt @@ -2,9 +2,9 @@ package com.tomasrepcik.sensorbox.domain.paired import com.tomasrepcik.sensorbox.core.error.DiagnosticLogger import com.tomasrepcik.sensorbox.core.error.toDiagnosticEvent -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionStore -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementStopReason -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementStopped +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStopReason +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStopped +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStore import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,7 +17,7 @@ import javax.inject.Singleton @Singleton class PhoneRecordingSessionObserver @Inject constructor( - private val sessionStore: MeasurementSessionStore, + private val sessionStore: RecordingSessionStore, private val pairedRecordingCoordinator: PairedRecordingCoordinator, private val diagnosticLogger: DiagnosticLogger, ) { @@ -31,17 +31,17 @@ class PhoneRecordingSessionObserver @Inject constructor( } } - private suspend fun onStopped(event: MeasurementStopped) { + private suspend fun onStopped(event: RecordingSessionStopped) { event.result.errorOrNull()?.let { error -> diagnosticLogger.record(error.toDiagnosticEvent()) } - val reason = event.reason.toAutomaticWearReason() ?: return + val reason = event.reason.toAutomaticWatchReason() ?: return pairedRecordingCoordinator.onAutomaticPhoneStop(event.sessionId, reason) } - private fun MeasurementStopReason.toAutomaticWearReason(): WearStopReason? = when (this) { - MeasurementStopReason.USER_REQUEST -> null - MeasurementStopReason.DURATION_EXPIRED -> WearStopReason.DURATION_EXPIRED - MeasurementStopReason.LOW_BATTERY -> WearStopReason.LOW_BATTERY - MeasurementStopReason.SOURCE_FAILURE -> WearStopReason.SOURCE_FAILURE - MeasurementStopReason.SERVICE_DESTROYED -> WearStopReason.SERVICE_DESTROYED + private fun RecordingSessionStopReason.toAutomaticWatchReason(): WearStopReason? = when (this) { + RecordingSessionStopReason.USER_REQUEST -> null + RecordingSessionStopReason.DURATION_EXPIRED -> WearStopReason.DURATION_EXPIRED + RecordingSessionStopReason.LOW_BATTERY -> WearStopReason.LOW_BATTERY + RecordingSessionStopReason.SOURCE_FAILURE -> WearStopReason.SOURCE_FAILURE + RecordingSessionStopReason.SERVICE_DESTROYED -> WearStopReason.SERVICE_DESTROYED } } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearCommandHandler.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchCommandHandler.kt similarity index 72% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearCommandHandler.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchCommandHandler.kt index 6470a02..ba04a1e 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearCommandHandler.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchCommandHandler.kt @@ -7,20 +7,20 @@ import com.tomasrepcik.sensorbox.core.error.AppError import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.appResult -import com.tomasrepcik.sensorbox.domain.sensors.WearSensorCatalogStore +import com.tomasrepcik.sensorbox.domain.sensors.WatchSensorCatalogStore import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_APP_CAPABILITY import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_MESSAGE_PATH import com.tomasrepcik.sensorbox.wearoslib.protocol.SendWearCommandUseCase import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOutcome import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class PhoneWearCommandHandler @Inject constructor( +class PhoneWatchCommandHandler @Inject constructor( @ApplicationContext private val context: Context, - private val wearSensorCatalog: WearSensorCatalogStore, - private val recordingResults: WearRecordingResultInbox, + private val watchSensorCatalog: WatchSensorCatalogStore, + private val recordingResults: WatchRecordingResultInbox, private val pairedRecordingCoordinator: PairedRecordingCoordinator, private val sendCommand: SendWearCommandUseCase, ) { @@ -28,7 +28,7 @@ class PhoneWearCommandHandler @Inject constructor( WearCommand.LaunchPhone -> launchPhone() is WearCommand.AvailableSensors -> { - wearSensorCatalog.update(command.sensors) + watchSensorCatalog.update(command.sensors) AppResult.success(Unit) } @@ -37,28 +37,29 @@ class PhoneWearCommandHandler @Inject constructor( AppResult.success(Unit) } - is WearCommand.StopRecording -> stopFromWear(command) + is WearCommand.StopRecording -> stopFromWatch(command) WearCommand.RequestAvailableSensors, WearCommand.SyncMeasurements, is WearCommand.StartRecording, - -> AppResult.failure(AppError(AppErrorCode.VALIDATION, "Handle unsupported phone Wear command")) + -> AppResult.failure(AppError(AppErrorCode.VALIDATION, "Handle unsupported phone watch command")) } - private fun launchPhone(): AppResult = appResult(AppErrorCode.EXTERNAL_ACTION, "Launch phone app from Wear") { - context.startActivity( - Intent(context, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP - }, - ) - } + private fun launchPhone(): AppResult = + appResult(AppErrorCode.EXTERNAL_ACTION, "Launch phone app from watch") { + context.startActivity( + Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + }, + ) + } - private suspend fun stopFromWear(command: WearCommand.StopRecording): AppResult { + private suspend fun stopFromWatch(command: WearCommand.StopRecording): AppResult { val stopResult = pairedRecordingCoordinator.stopFromWatch(command.sessionId, command.reason) val error = stopResult.errorOrNull() val result = WearCommand.RecordingResult( sessionId = command.sessionId, - action = WearRecordingAction.STOP, + operation = WearRecordingOperation.STOP, outcome = if (stopResult.isSuccess) { WearRecordingOutcome.SUCCEEDED } else { diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearMessageDispatcher.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchMessageDispatcher.kt similarity index 88% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearMessageDispatcher.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchMessageDispatcher.kt index 3c8fbc1..ee2537f 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearMessageDispatcher.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchMessageDispatcher.kt @@ -8,8 +8,8 @@ import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.PHONE_MESSAGE_PATH import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommandCodec import javax.inject.Inject -class PhoneWearMessageDispatcher @Inject constructor( - private val commandHandler: PhoneWearCommandHandler, +class PhoneWatchMessageDispatcher @Inject constructor( + private val commandHandler: PhoneWatchCommandHandler, private val diagnosticLogger: DiagnosticLogger, ) { suspend fun dispatch(path: String, payload: ByteArray): AppResult { diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WearRecordingResultInbox.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WatchRecordingResultInbox.kt similarity index 68% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WearRecordingResultInbox.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WatchRecordingResultInbox.kt index 03412d0..32c2c14 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WearRecordingResultInbox.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WatchRecordingResultInbox.kt @@ -1,7 +1,7 @@ package com.tomasrepcik.sensorbox.domain.paired import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import java.util.concurrent.ConcurrentHashMap @@ -9,7 +9,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class WearRecordingResultInbox @Inject constructor() { +class WatchRecordingResultInbox @Inject constructor() { private val results = ConcurrentHashMap() private val updates = MutableSharedFlow(extraBufferCapacity = BUFFER_SIZE) @@ -18,19 +18,19 @@ class WearRecordingResultInbox @Inject constructor() { updates.tryEmit(result) } - fun clear(sessionId: String, action: WearRecordingAction) { - results.remove(Key(sessionId, action)) + fun clear(sessionId: String, operation: WearRecordingOperation) { + results.remove(Key(sessionId, operation)) } - suspend fun await(sessionId: String, action: WearRecordingAction): WearCommand.RecordingResult { - val key = Key(sessionId, action) + suspend fun await(sessionId: String, operation: WearRecordingOperation): WearCommand.RecordingResult { + val key = Key(sessionId, operation) results[key]?.let { return it } return updates.first { result -> result.key() == key } } - private fun WearCommand.RecordingResult.key() = Key(sessionId, action) + private fun WearCommand.RecordingResult.key() = Key(sessionId, operation) - private data class Key(val sessionId: String, val action: WearRecordingAction) + private data class Key(val sessionId: String, val operation: WearRecordingOperation) private companion object { const val BUFFER_SIZE = 16 diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/PhoneRecordingController.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/PhoneRecordingController.kt similarity index 54% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/PhoneRecordingController.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/PhoneRecordingController.kt index 904f109..ec0f64f 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/PhoneRecordingController.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/PhoneRecordingController.kt @@ -1,4 +1,4 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.content.Context import android.content.Intent @@ -9,15 +9,15 @@ import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.appResult import com.tomasrepcik.sensorbox.core.error.flatMap import com.tomasrepcik.sensorbox.core.storage.DocumentStorage -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementIntentFactory -import com.tomasrepcik.sensorbox.sensorservices.services.MeasurementService +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingIntentFactory +import com.tomasrepcik.sensorbox.sensorservices.services.RecordingService import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton interface PhoneRecordingController { - fun start(sessionId: String, request: MeasurementRequest): AppResult + fun start(sessionId: String, request: RecordingSetup): AppResult fun stop(sessionId: String, reason: WearStopReason): AppResult @@ -29,31 +29,30 @@ interface PhoneRecordingController { @Singleton class AndroidPhoneRecordingController @Inject constructor( @ApplicationContext private val context: Context, - private val intentFactory: MeasurementIntentFactory, + private val intentFactory: RecordingIntentFactory, private val documentStorage: DocumentStorage, ) : PhoneRecordingController { - override fun start(sessionId: String, request: MeasurementRequest): AppResult = - documentStorage - .hasConfiguredDirectory() - .flatMap { storageReady -> - if (!storageReady) { - AppResult.failure(AppError(AppErrorCode.STORAGE, "Start phone recording storage")) - } else { - startForegroundRecording(sessionId, request) - } + override fun start(sessionId: String, request: RecordingSetup): AppResult = documentStorage + .hasConfiguredDirectory() + .flatMap { storageReady -> + if (!storageReady) { + AppResult.failure(AppError(AppErrorCode.STORAGE, "Start phone recording storage")) + } else { + startForegroundRecording(sessionId, request) } + } private fun startForegroundRecording( sessionId: String, - request: MeasurementRequest, + request: RecordingSetup, ): AppResult = appResult( - AppErrorCode.MEASUREMENT, + AppErrorCode.RECORDING, "Start phone recording", ) { val folderName = intentFactory.newFolderName(request.customName) - val launchRequest = request.toLaunchRequest(sessionId, folderName) - ContextCompat.startForegroundService(context, intentFactory.create(launchRequest)) - StartedPhoneRecording(sessionId, folderName, launchRequest.durationMillis) + val recordingRequest = request.toRecordingRequest(sessionId, folderName) + ContextCompat.startForegroundService(context, intentFactory.create(recordingRequest)) + StartedPhoneRecording(sessionId, folderName, recordingRequest.durationMillis) } override fun stop(sessionId: String, reason: WearStopReason): AppResult = stopService("Stop phone recording") @@ -61,19 +60,19 @@ class AndroidPhoneRecordingController @Inject constructor( override fun stopCurrent(reason: WearStopReason): AppResult = stopService("Stop current phone recording") override fun annotate(text: String, timestampMillis: Long): AppResult = appResult( - AppErrorCode.MEASUREMENT, - "Request measurement annotation", + AppErrorCode.RECORDING, + "Request recording annotation", ) { - val intent = Intent(context, MeasurementService::class.java) - .setAction(MeasurementService.ACTION_ANNOTATE) - .putExtra(MeasurementService.ANNOTATION_TIME, timestampMillis) - .putExtra(MeasurementService.ANNOTATION_TEXT, text) + val intent = Intent(context, RecordingService::class.java) + .setAction(RecordingService.ACTION_ANNOTATE) + .putExtra(RecordingService.ANNOTATION_TIME, timestampMillis) + .putExtra(RecordingService.ANNOTATION_TEXT, text) context.startService(intent) } - private fun stopService(operation: String): AppResult = appResult(AppErrorCode.MEASUREMENT, operation) { - val stopIntent = Intent(context, MeasurementService::class.java) - .setAction(MeasurementService.ACTION_STOP) + private fun stopService(operation: String): AppResult = appResult(AppErrorCode.RECORDING, operation) { + val stopIntent = Intent(context, RecordingService::class.java) + .setAction(RecordingService.ACTION_STOP_RECORDING) context.startService(stopIntent) } } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingArchiveRepository.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingArchiveRepository.kt new file mode 100644 index 0000000..bd58020 --- /dev/null +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingArchiveRepository.kt @@ -0,0 +1,23 @@ +package com.tomasrepcik.sensorbox.domain.recording + +import android.content.Intent +import com.tomasrepcik.sensorbox.core.error.AppResult +import com.tomasrepcik.sensorbox.core.storage.DocumentStorage +import javax.inject.Inject + +interface RecordingArchiveRepository { + fun isSelected(): AppResult + + fun path(): AppResult + + fun select(resultIntent: Intent): AppResult +} + +class DocumentRecordingArchiveRepository @Inject constructor(private val storage: DocumentStorage) : + RecordingArchiveRepository { + override fun isSelected(): AppResult = storage.hasConfiguredDirectory() + + override fun path(): AppResult = storage.displayPath() + + override fun select(resultIntent: Intent): AppResult = storage.persistRootAccess(resultIntent) +} diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementControlUseCase.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingControlUseCase.kt similarity index 73% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementControlUseCase.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingControlUseCase.kt index 932044a..38e1a29 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementControlUseCase.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingControlUseCase.kt @@ -1,4 +1,4 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.time.EpochClock @@ -7,19 +7,19 @@ import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import javax.inject.Inject interface RecordingControlUseCase { - suspend fun start(request: MeasurementRequest): AppResult + suspend fun start(request: RecordingSetup): AppResult suspend fun stop(): AppResult fun annotate(text: String): AppResult } -class MeasurementControlUseCase @Inject constructor( +class DefaultRecordingControlUseCase @Inject constructor( private val pairedRecordingCoordinator: PairedRecordingCoordinator, private val localController: PhoneRecordingController, private val clock: EpochClock, ) : RecordingControlUseCase { - override suspend fun start(request: MeasurementRequest): AppResult = pairedRecordingCoordinator.start(request) + override suspend fun start(request: RecordingSetup): AppResult = pairedRecordingCoordinator.start(request) override suspend fun stop(): AppResult = pairedRecordingCoordinator.stop(WearStopReason.USER_REQUEST) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementPermissionUseCase.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingPermissionUseCase.kt similarity index 51% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementPermissionUseCase.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingPermissionUseCase.kt index f14ec46..fdb7116 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementPermissionUseCase.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingPermissionUseCase.kt @@ -1,4 +1,4 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.Manifest import android.content.Context @@ -8,20 +8,20 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -fun interface MeasurementPermissionsUseCase { - fun missingPermissions(request: MeasurementRequest): Set +fun interface RecordingPermissionsUseCase { + fun missingPermissions(request: RecordingSetup): Set } -class MeasurementPermissionUseCase @Inject constructor(@ApplicationContext private val context: Context) : - MeasurementPermissionsUseCase { - override fun missingPermissions(request: MeasurementRequest): Set = - requiredMeasurementPermissions(request, Build.VERSION.SDK_INT).filterNot(::isGranted).toSet() +class RecordingPermissionUseCase @Inject constructor(@ApplicationContext private val context: Context) : + RecordingPermissionsUseCase { + override fun missingPermissions(request: RecordingSetup): Set = + requiredRecordingPermissions(request, Build.VERSION.SDK_INT).filterNot(::isGranted).toSet() private fun isGranted(permission: String): Boolean = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED } -internal fun requiredMeasurementPermissions(request: MeasurementRequest, sdkInt: Int): Set = buildSet { +internal fun requiredRecordingPermissions(request: RecordingSetup, sdkInt: Int): Set = buildSet { if (sdkInt >= 33) add(Manifest.permission.POST_NOTIFICATIONS) if (request.includesGps) add(Manifest.permission.ACCESS_FINE_LOCATION) if (request.activityRecognition && sdkInt >= 29) add(Manifest.permission.ACTIVITY_RECOGNITION) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementRequest.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingSetup.kt similarity index 78% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementRequest.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingSetup.kt index 56de7e9..74abde8 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementRequest.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingSetup.kt @@ -1,10 +1,10 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.hardware.SensorManager -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingRequest -data class MeasurementRequest( +data class RecordingSetup( val sensorIds: Set, val includesGps: Boolean, val samplingPeriodIndex: Int, @@ -12,8 +12,8 @@ data class MeasurementRequest( val useWakeLock: Boolean, val gpsIntervalSeconds: Int, val gpsMinDistanceMeters: Int, - val wearSensorIds: Set = emptySet(), - val wearIncludesGps: Boolean = false, + val watchSensorIds: Set = emptySet(), + val watchIncludesGps: Boolean = false, val customName: String = "", val durationSeconds: Int = 0, val notes: List = emptyList(), @@ -22,7 +22,7 @@ data class MeasurementRequest( val activityRecognitionPeriodSeconds: Int = 30, val significantMotion: Boolean = false, ) { - fun toLaunchRequest(sessionId: String, folderName: String) = MeasurementLaunchRequest( + fun toRecordingRequest(sessionId: String, folderName: String) = RecordingRequest( sessionId = sessionId, folderName = folderName, useInternalStorage = false, @@ -55,9 +55,9 @@ data class MeasurementRequest( data class StartedPhoneRecording(val sessionId: String, val folderName: String, val durationMillis: Long) -fun StartedPhoneRecording.toWearRecordingRequest(request: MeasurementRequest) = WearRecordingRequest( +fun StartedPhoneRecording.toWatchRecordingRequest(request: RecordingSetup) = WearRecordingRequest( folderName = folderName, - sensorIds = request.wearSensorIds.sorted(), - includesGps = request.wearIncludesGps, + sensorIds = request.watchSensorIds.sorted(), + includesGps = request.watchIncludesGps, durationMillis = durationMillis, ) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorCatalogStore.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorCatalogStore.kt similarity index 94% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorCatalogStore.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorCatalogStore.kt index cbf57ec..caa3da1 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorCatalogStore.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorCatalogStore.kt @@ -8,7 +8,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class WearSensorCatalogStore @Inject constructor() { +class WatchSensorCatalogStore @Inject constructor() { private val mutableSensors = MutableStateFlow>(emptyList()) private val mutableAvailable = MutableStateFlow(false) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/ReceiveWearFileUseCase.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/ReceiveWatchFileUseCase.kt similarity index 91% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/ReceiveWearFileUseCase.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/ReceiveWatchFileUseCase.kt index 7216b5a..e0d5172 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/ReceiveWearFileUseCase.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/ReceiveWatchFileUseCase.kt @@ -10,9 +10,9 @@ import com.tomasrepcik.sensorbox.wearoslib.files.WearFilePathCodec import com.tomasrepcik.sensorbox.wearoslib.files.WearFileTransferClient import javax.inject.Inject -class ReceiveWearFileUseCase @Inject constructor( +class ReceiveWatchFileUseCase @Inject constructor( private val transferClient: WearFileTransferClient, - private val destination: WearFileDestination, + private val destination: WatchFileDestination, ) { suspend operator fun invoke(channel: ChannelClient.Channel): AppResult = WearFilePathCodec.decode(channel.path).suspendFlatMap { metadata -> @@ -21,7 +21,7 @@ class ReceiveWearFileUseCase @Inject constructor( AppResult.success(Unit) } else { AppResult.failure( - AppError(AppErrorCode.STORAGE, "Receive Wear file"), + AppError(AppErrorCode.STORAGE, "Receive watch file"), ) } }.suspendFlatMap { diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/WearFileDestination.kt b/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/WatchFileDestination.kt similarity index 91% rename from app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/WearFileDestination.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/WatchFileDestination.kt index 5094bf1..cd02ee0 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/WearFileDestination.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/domain/sync/WatchFileDestination.kt @@ -16,7 +16,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class WearFileDestination @Inject constructor( +class WatchFileDestination @Inject constructor( @ApplicationContext private val context: Context, private val documentStorage: DocumentStorage, ) { @@ -47,15 +47,15 @@ class WearFileDestination @Inject constructor( ) private fun copyToDebugDirectory(measurementName: String, fileName: String, input: InputStream): AppResult { - if (!isDebugBuild()) return AppResult.failure(AppError(AppErrorCode.STORAGE, "Copy debug Wear file")) - return appResult(AppErrorCode.STORAGE, "Prepare debug Wear directory") { + if (!isDebugBuild()) return AppResult.failure(AppError(AppErrorCode.STORAGE, "Copy debug watch file")) + return appResult(AppErrorCode.STORAGE, "Prepare debug watch directory") { val directory = File(context.filesDir, "$APP_DIRECTORY/$measurementName") directory to (directory.isDirectory || directory.mkdirs()) }.flatMap { (directory, ready) -> if (!ready) { - AppResult.failure(AppError(AppErrorCode.STORAGE, "Prepare debug Wear directory")) + AppResult.failure(AppError(AppErrorCode.STORAGE, "Prepare debug watch directory")) } else { - appResult(AppErrorCode.STORAGE, "Copy debug Wear file") { + appResult(AppErrorCode.STORAGE, "Copy debug watch file") { File(directory, fileName).outputStream().use(input::copyTo) Unit } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/ActiveMeasurementScreen.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/ActiveRecordingScreen.kt similarity index 91% rename from app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/ActiveMeasurementScreen.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/ActiveRecordingScreen.kt index 4d78b98..e50dba8 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/ActiveMeasurementScreen.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/ActiveRecordingScreen.kt @@ -52,12 +52,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tomasrepcik.sensorbox.R import com.tomasrepcik.sensorbox.core.format.ValueFormats -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState import com.tomasrepcik.sensorbox.ui.theme.SensorBoxRecording @Composable -fun ActiveMeasurementScreen(state: RecordingState, onIntent: (RecordingIntent) -> Unit, modifier: Modifier = Modifier) { - val session = state.session as? MeasurementSessionState.Running ?: return +fun ActiveRecordingScreen(state: RecordingState, onIntent: (RecordingIntent) -> Unit, modifier: Modifier = Modifier) { + val session = state.session as? RecordingSessionState.Running ?: return Column( modifier = modifier.fillMaxSize(), ) { @@ -68,14 +68,14 @@ fun ActiveMeasurementScreen(state: RecordingState, onIntent: (RecordingIntent) - .verticalScroll(rememberScrollState()) .padding(horizontal = 24.dp), ) { - MeasurementTimer(state.elapsedSeconds, session.folderName) + RecordingTimer(state.elapsedSeconds, session.folderName) SensorBoxSettingsDivider() - MeasurementSummary(state, session) - SensorBoxSettingsSection(stringResource(R.string.measurement_markers_category)) + RecordingSummary(state, session) + SensorBoxSettingsSection(stringResource(R.string.recording_annotations_category)) AnnotationEditor(onIntent) Spacer(Modifier.height(24.dp)) } - ActiveMeasurementActionBar(onIntent) + ActiveRecordingTopBar(onIntent) } } @@ -167,7 +167,7 @@ private fun RecordingIndicator() { } @Composable -private fun MeasurementTimer(elapsedSeconds: Long, folderName: String) { +private fun RecordingTimer(elapsedSeconds: Long, folderName: String) { Column( modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -180,7 +180,7 @@ private fun MeasurementTimer(elapsedSeconds: Long, folderName: String) { } @Composable -private fun MeasurementSummary(state: RecordingState, session: MeasurementSessionState.Running) { +private fun RecordingSummary(state: RecordingState, session: RecordingSessionState.Running) { var expanded by remember { mutableStateOf(false) } val sources = recordingSourceNames(state, session, LocalContext.current.resources) Column(Modifier.fillMaxWidth()) { @@ -239,7 +239,7 @@ private fun RecordingSourceList(sources: List) { private fun recordingSourceNames( state: RecordingState, - session: MeasurementSessionState.Running, + session: RecordingSessionState.Running, resources: Resources, ): List = buildList { session.sensorIds.forEach { sensorId -> @@ -252,14 +252,14 @@ private fun recordingSourceNames( } @Composable -private fun ActiveMeasurementActionBar(onIntent: (RecordingIntent) -> Unit) { +private fun ActiveRecordingTopBar(onIntent: (RecordingIntent) -> Unit) { Box( Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 14.dp), contentAlignment = Alignment.Center, ) { SensorBoxDangerButton( label = stringResource(R.string.stop_and_save), - onClick = { onIntent(RecordingIntent.StopMeasurement) }, + onClick = { onIntent(RecordingIntent.StopRecording) }, modifier = Modifier.widthIn(min = 176.dp, max = 240.dp), ) } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainContract.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainContract.kt index 3bcd9de..291fed0 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainContract.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainContract.kt @@ -1,17 +1,17 @@ package com.tomasrepcik.sensorbox.presentation.main import androidx.navigation3.runtime.NavKey -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState import kotlinx.serialization.Serializable @Serializable enum class MainRoute : NavKey { ONBOARDING, RECORD, - ACTIVE_MEASUREMENT, + ACTIVE_RECORDING, SENSOR_DETAILS, SENSOR_PREVIEW, - SETUP, + RECORDING_SETUP, MEASUREMENTS, MEASUREMENT_DETAILS, MEASUREMENT_FILE, @@ -23,7 +23,7 @@ enum class MainRoute : NavKey { data class MainState( val route: MainRoute = MainRoute.ONBOARDING, - val session: MeasurementSessionState = MeasurementSessionState.Idle, + val session: RecordingSessionState = RecordingSessionState.Idle, val keepScreenAwake: Boolean = false, val hasLoadedPreferences: Boolean = false, ) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainViewModel.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainViewModel.kt index 90f8f9f..3d99276 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainViewModel.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MainViewModel.kt @@ -3,7 +3,7 @@ package com.tomasrepcik.sensorbox.presentation.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesRepository -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionStore +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -14,7 +14,7 @@ import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val preferencesRepository: AppPreferencesRepository, - private val sessionStore: MeasurementSessionStore, + private val sessionStore: RecordingSessionStore, ) : ViewModel() { private val mutableState = MutableStateFlow(MainState()) private var hasChosenInitialRoute = false diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingContract.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingContract.kt index 8d418fd..ca26fd7 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingContract.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingContract.kt @@ -2,7 +2,11 @@ package com.tomasrepcik.sensorbox.presentation.main import com.tomasrepcik.sensorbox.core.error.AppErrorCode -data class OnboardingState(val page: Int = 0, val storagePath: String? = null, val errorCode: AppErrorCode? = null) +data class OnboardingState( + val page: Int = 0, + val recordingArchivePath: String? = null, + val errorCode: AppErrorCode? = null, +) sealed interface OnboardingIntent { data object AdvanceOnboarding : OnboardingIntent @@ -11,11 +15,11 @@ sealed interface OnboardingIntent { data object OpenPrivacyPolicy : OnboardingIntent data object OpenTermsOfUse : OnboardingIntent data object RequestBatteryOptimizationExemption : OnboardingIntent - data object ChooseStorage : OnboardingIntent + data object ChooseRecordingArchive : OnboardingIntent } sealed interface OnboardingEffect { - data object PickStorageDirectory : OnboardingEffect + data object PickRecordingArchive : OnboardingEffect data object OpenPrivacyPolicy : OnboardingEffect data object OpenTermsOfUse : OnboardingEffect data object RequestBatteryOptimizationExemption : OnboardingEffect diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreen.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreen.kt index da988c3..5255601 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreen.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingScreen.kt @@ -81,7 +81,7 @@ private fun OnboardingLayout( } OnboardingControls( pageIndex = pageIndex, - hasStorage = state.storagePath != null, + archiveSelected = state.recordingArchivePath != null, isLandscape = isLandscape, onIntent = onIntent, modifier = Modifier @@ -186,7 +186,7 @@ private fun PortraitOnboardingMessage( Spacer(Modifier.height(24.dp)) OnboardingText(page) Spacer(Modifier.height(18.dp)) - OnboardingPageActions(page.action, state.storagePath, onIntent) + OnboardingPageButtons(page.prompt, state.recordingArchivePath, onIntent) } } @@ -213,7 +213,7 @@ private fun LandscapeOnboardingMessage( ) { OnboardingText(page) Spacer(Modifier.height(12.dp)) - OnboardingPageActions(page.action, state.storagePath, onIntent) + OnboardingPageButtons(page.prompt, state.recordingArchivePath, onIntent) } } } @@ -252,17 +252,17 @@ private fun OnboardingText(page: OnboardingPage) { } @Composable -private fun OnboardingPageActions(action: OnboardingAction, path: String?, onIntent: (OnboardingIntent) -> Unit) { - when (action) { - OnboardingAction.NONE -> Unit - OnboardingAction.POLICIES -> PolicyActions(onIntent) - OnboardingAction.BATTERY -> BatteryAction(onIntent) - OnboardingAction.STORAGE -> StorageAction(path, onIntent) +private fun OnboardingPageButtons(prompt: OnboardingPrompt, path: String?, onIntent: (OnboardingIntent) -> Unit) { + when (prompt) { + OnboardingPrompt.NONE -> Unit + OnboardingPrompt.POLICIES -> PolicyButtons(onIntent) + OnboardingPrompt.BATTERY -> BatteryButton(onIntent) + OnboardingPrompt.ARCHIVE -> ArchiveButton(path, onIntent) } } @Composable -private fun PolicyActions(onIntent: (OnboardingIntent) -> Unit) { +private fun PolicyButtons(onIntent: (OnboardingIntent) -> Unit) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { SensorBoxSecondaryButton( label = stringResource(R.string.intro_policy_button), @@ -280,7 +280,7 @@ private fun PolicyActions(onIntent: (OnboardingIntent) -> Unit) { } @Composable -private fun BatteryAction(onIntent: (OnboardingIntent) -> Unit) { +private fun BatteryButton(onIntent: (OnboardingIntent) -> Unit) { SensorBoxSecondaryButton( label = stringResource(R.string.intro_battery_action), onClick = { onIntent(OnboardingIntent.RequestBatteryOptimizationExemption) }, @@ -290,20 +290,20 @@ private fun BatteryAction(onIntent: (OnboardingIntent) -> Unit) { } @Composable -private fun StorageAction(path: String?, onIntent: (OnboardingIntent) -> Unit) { +private fun ArchiveButton(path: String?, onIntent: (OnboardingIntent) -> Unit) { Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { SensorBoxSecondaryButton( label = stringResource( - if (path == null) R.string.intro_storage_action else R.string.intro_storage_change_action, + if (path == null) R.string.intro_archive_action else R.string.intro_archive_change_action, ), - onClick = { onIntent(OnboardingIntent.ChooseStorage) }, + onClick = { onIntent(OnboardingIntent.ChooseRecordingArchive) }, modifier = Modifier.fillMaxWidth(), contentColor = MaterialTheme.colorScheme.onPrimaryContainer, ) if (path != null) { Spacer(Modifier.height(10.dp)) Text( - stringResource(R.string.intro_storage_selected, path), + stringResource(R.string.intro_archive_selected, path), color = MaterialTheme.colorScheme.onPrimaryContainer, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium, @@ -315,7 +315,7 @@ private fun StorageAction(path: String?, onIntent: (OnboardingIntent) -> Unit) { @Composable private fun OnboardingControls( pageIndex: Int, - hasStorage: Boolean, + archiveSelected: Boolean, isLandscape: Boolean, onIntent: (OnboardingIntent) -> Unit, modifier: Modifier = Modifier, @@ -327,7 +327,7 @@ private fun OnboardingControls( OnboardingPrimaryButton( label = stringResource(if (isLastPage) R.string.intro_finish else R.string.next), onClick = { onIntent(onboardingForwardIntent(isLastPage)) }, - enabled = !isLastPage || hasStorage, + enabled = !isLastPage || archiveSelected, ) } } @@ -357,11 +357,11 @@ private data class OnboardingPage( @StringRes val title: Int, @StringRes val body: Int, @DrawableRes val image: Int, - val action: OnboardingAction = OnboardingAction.NONE, + val prompt: OnboardingPrompt = OnboardingPrompt.NONE, val tintIllustration: Boolean = true, ) -private enum class OnboardingAction { NONE, POLICIES, BATTERY, STORAGE } +private enum class OnboardingPrompt { NONE, POLICIES, BATTERY, ARCHIVE } private val ONBOARDING_MAX_WIDTH = 960.dp private val ONBOARDING_MESSAGE_MAX_WIDTH = 520.dp @@ -383,7 +383,7 @@ private val ONBOARDING_PAGES = listOf( R.string.intro_policy_title, R.string.intro_policy_body, R.drawable.ic_onboarding_policy, - OnboardingAction.POLICIES, + OnboardingPrompt.POLICIES, ), OnboardingPage( R.string.intro_lifecycle_title, @@ -394,12 +394,12 @@ private val ONBOARDING_PAGES = listOf( R.string.intro_battery_title, R.string.intro_battery_body, R.drawable.ic_onboarding_battery, - OnboardingAction.BATTERY, + OnboardingPrompt.BATTERY, ), OnboardingPage( - R.string.intro_storage_title, - R.string.intro_storage_body, + R.string.intro_archive_title, + R.string.intro_archive_body, R.drawable.ic_onboarding_storage, - OnboardingAction.STORAGE, + OnboardingPrompt.ARCHIVE, ), ) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingViewModel.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingViewModel.kt index 0141d34..cd4622e 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingViewModel.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/OnboardingViewModel.kt @@ -8,7 +8,7 @@ import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesIntent import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesRepository -import com.tomasrepcik.sensorbox.domain.measurement.DocumentStorageGateway +import com.tomasrepcik.sensorbox.domain.recording.RecordingArchiveRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -21,10 +21,10 @@ import javax.inject.Inject @HiltViewModel class OnboardingViewModel @Inject constructor( private val preferencesRepository: AppPreferencesRepository, - private val documentStorage: DocumentStorageGateway, + private val recordingArchive: RecordingArchiveRepository, ) : ViewModel() { private val mutableState = MutableStateFlow( - OnboardingState(storagePath = documentStorage.displayPath().getOrNull()), + OnboardingState(recordingArchivePath = recordingArchive.path().getOrNull()), ) private val mutableEffects = Channel(Channel.BUFFERED) @@ -45,7 +45,7 @@ class OnboardingViewModel @Inject constructor( OnboardingIntent.CompleteOnboarding -> completeOnboarding() - OnboardingIntent.ChooseStorage -> mutableEffects.trySend(OnboardingEffect.PickStorageDirectory) + OnboardingIntent.ChooseRecordingArchive -> mutableEffects.trySend(OnboardingEffect.PickRecordingArchive) OnboardingIntent.OpenPrivacyPolicy -> mutableEffects.trySend(OnboardingEffect.OpenPrivacyPolicy) @@ -56,18 +56,18 @@ class OnboardingViewModel @Inject constructor( } } - fun handleStorageResult(resultIntent: Intent?) { - val result = resultIntent?.let(documentStorage::persist) ?: AppResult.failure( - AppError(AppErrorCode.STORAGE, "Select onboarding storage directory"), + fun handleRecordingArchiveResult(resultIntent: Intent?) { + val result = resultIntent?.let(recordingArchive::select) ?: AppResult.failure( + AppError(AppErrorCode.STORAGE, "Select onboarding recording archive"), ) mutableState.value = state.value.copy( - storagePath = documentStorage.displayPath().getOrNull(), + recordingArchivePath = recordingArchive.path().getOrNull(), errorCode = result.errorOrNull()?.code, ) } private fun completeOnboarding() { - if (documentStorage.hasStorage().getOrNull() != true) { + if (recordingArchive.isSelected().getOrNull() != true) { mutableState.value = state.value.copy(errorCode = AppErrorCode.STORAGE) return } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreen.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreen.kt index c2f52c8..f8bc292 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreen.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreen.kt @@ -40,7 +40,7 @@ fun RecordScreen(state: RecordingState, onIntent: (RecordingIntent) -> Unit, mod Box(Modifier.weight(1f)) { RecordContent(state, onIntent) } - SensorSelectionActionBar(state, onIntent) + SensorSelectionTopBar(state, onIntent) } } @@ -80,26 +80,26 @@ private fun RecordContent(state: RecordingState, onIntent: (RecordingIntent) -> SourceDivider() } } - if (state.isWearConnected) { + if (state.isWatchConnected) { item { WearSectionHeader() } item { GpsRow( - selected = state.wearIncludesGps, - onToggle = { onIntent(RecordingIntent.ToggleWearGps) }, + selected = state.watchIncludesGps, + onToggle = { onIntent(RecordingIntent.ToggleWatchGps) }, ) } item { SourceDivider() } - items(state.wearSensors, key = { "wear_${it.type}" }) { sensor -> + items(state.watchSensors, key = { "watch_${it.type}" }) { sensor -> Column { SensorRow( sensor = sensor, - selected = sensor.type in state.selectedWearSensorIds, - onToggle = { onIntent(RecordingIntent.ToggleWearSensor(sensor.type)) }, + selected = sensor.type in state.selectedWatchSensorIds, + onToggle = { onIntent(RecordingIntent.ToggleWatchSensor(sensor.type)) }, onInfo = { onIntent( RecordingIntent.OpenSensorDetails( sensor.type, - RecordingSensorSource.WEAR, + RecordingDevice.WATCH, ), ) }, @@ -115,7 +115,7 @@ private fun RecordContent(state: RecordingState, onIntent: (RecordingIntent) -> @Composable private fun WearSectionHeader() { Text( - stringResource(R.string.wear_sensors), + stringResource(R.string.watch_sensors), modifier = Modifier.fillMaxWidth().padding(top = 28.dp, bottom = 8.dp), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -217,7 +217,7 @@ private fun SourceDivider() { } @Composable -private fun SensorSelectionActionBar( +private fun SensorSelectionTopBar( state: RecordingState, onIntent: (RecordingIntent) -> Unit, modifier: Modifier = Modifier, @@ -234,7 +234,7 @@ private fun SensorSelectionActionBar( ) { SensorBoxPrimaryButton( label = stringResource(R.string.continue_action), - onClick = { onIntent(RecordingIntent.OpenMeasurementSetup) }, + onClick = { onIntent(RecordingIntent.OpenRecordingSetup) }, modifier = Modifier.fillMaxWidth(), enabled = sensorCount > 0, ) @@ -243,9 +243,9 @@ private fun SensorSelectionActionBar( } private fun selectedSourceCount(state: RecordingState): Int = state.selectedSensorIds.size + - state.selectedWearSensorIds.size + + state.selectedWatchSensorIds.size + (if (state.includesGps) 1 else 0) + - (if (state.wearIncludesGps) 1 else 0) + + (if (state.watchIncludesGps) 1 else 0) + (if (state.activityRecognition) 1 else 0) + (if (state.significantMotion) 1 else 0) @@ -287,19 +287,19 @@ fun RecordingMessageText(message: RecordingMessage) { @StringRes private fun messageTitleResource(message: RecordingMessage): Int = when (message) { RecordingMessage.PICK_AT_LEAST_ONE_SOURCE -> R.string.message_pick_source_title - RecordingMessage.STORAGE_REQUIRED -> R.string.message_storage_required_title + RecordingMessage.RECORDING_ARCHIVE_REQUIRED -> R.string.message_recording_archive_required_title RecordingMessage.PERMISSION_REQUIRED -> R.string.message_permission_required_title - RecordingMessage.WEAR_PERMISSION_REQUIRED -> R.string.message_wear_permission_required_title - RecordingMessage.MEASUREMENT_FAILED -> R.string.message_measurement_failed_title + RecordingMessage.WATCH_PERMISSION_REQUIRED -> R.string.message_wear_permission_required_title + RecordingMessage.RECORDING_FAILED -> R.string.message_recording_failed_title RecordingMessage.NONE -> R.string.app_name } @StringRes private fun messageTextResource(message: RecordingMessage): Int = when (message) { RecordingMessage.PICK_AT_LEAST_ONE_SOURCE -> R.string.message_pick_source - RecordingMessage.STORAGE_REQUIRED -> R.string.message_storage_required + RecordingMessage.RECORDING_ARCHIVE_REQUIRED -> R.string.message_recording_archive_required RecordingMessage.PERMISSION_REQUIRED -> R.string.message_permission_required - RecordingMessage.WEAR_PERMISSION_REQUIRED -> R.string.message_wear_permission_required - RecordingMessage.MEASUREMENT_FAILED -> R.string.message_measurement_failed + RecordingMessage.WATCH_PERMISSION_REQUIRED -> R.string.message_wear_permission_required + RecordingMessage.RECORDING_FAILED -> R.string.message_recording_failed RecordingMessage.NONE -> R.string.app_name } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingContract.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingContract.kt index 772fc12..4c78933 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingContract.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingContract.kt @@ -2,33 +2,33 @@ package com.tomasrepcik.sensorbox.presentation.main import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.preferences.AppPreferences -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementRequest +import com.tomasrepcik.sensorbox.domain.recording.RecordingSetup import com.tomasrepcik.sensorbox.domain.sensors.SensorDescriptor -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState enum class RecordingMessage { NONE, PICK_AT_LEAST_ONE_SOURCE, - STORAGE_REQUIRED, + RECORDING_ARCHIVE_REQUIRED, PERMISSION_REQUIRED, - WEAR_PERMISSION_REQUIRED, - MEASUREMENT_FAILED, + WATCH_PERMISSION_REQUIRED, + RECORDING_FAILED, } -enum class RecordingSensorSource { +enum class RecordingDevice { PHONE, - WEAR, + WATCH, } data class RecordingState( val sensors: List = emptyList(), - val wearSensors: List = emptyList(), + val watchSensors: List = emptyList(), val detailsSensorType: Int? = null, - val detailsSensorSource: RecordingSensorSource = RecordingSensorSource.PHONE, + val detailsDevice: RecordingDevice = RecordingDevice.PHONE, val selectedSensorIds: Set = emptySet(), val includesGps: Boolean = false, - val selectedWearSensorIds: Set = emptySet(), - val wearIncludesGps: Boolean = false, + val selectedWatchSensorIds: Set = emptySet(), + val watchIncludesGps: Boolean = false, val customMeasurementName: String = "", val startDelaySeconds: Int = 0, val durationSeconds: Int = 0, @@ -37,26 +37,26 @@ data class RecordingState( val activityRecognition: Boolean = false, val activityRecognitionPeriodSeconds: Int = 30, val significantMotion: Boolean = false, - val storagePath: String? = null, + val recordingArchivePath: String? = null, val preferences: AppPreferences = AppPreferences(), - val session: MeasurementSessionState = MeasurementSessionState.Idle, + val session: RecordingSessionState = RecordingSessionState.Idle, val elapsedSeconds: Long = 0, - val isWearConnected: Boolean = false, + val isWatchConnected: Boolean = false, val isStarting: Boolean = false, val startCountdownSeconds: Int? = null, val message: RecordingMessage = RecordingMessage.NONE, val errorCode: AppErrorCode? = null, ) { - fun toMeasurementRequest() = MeasurementRequest( + fun toRecordingSetup() = RecordingSetup( sensorIds = selectedSensorIds, includesGps = includesGps, samplingPeriodIndex = preferences.recording.sensorSamplingPeriod, - stopOnLowBattery = preferences.recording.restrictMeasurementOnLowBattery, + stopOnLowBattery = preferences.recording.stopRecordingOnLowBattery, useWakeLock = preferences.recording.useWakeLock, gpsIntervalSeconds = preferences.recording.gpsIntervalSeconds, gpsMinDistanceMeters = preferences.recording.gpsMinDistanceMeters, - wearSensorIds = selectedWearSensorIds, - wearIncludesGps = wearIncludesGps, + watchSensorIds = selectedWatchSensorIds, + watchIncludesGps = watchIncludesGps, customName = customMeasurementName, durationSeconds = durationSeconds.coerceAtLeast(0), notes = notes.lines().map(String::trim).filter(String::isNotEmpty), @@ -71,18 +71,16 @@ data class RecordingState( sealed interface RecordingIntent { data class Navigate(val route: MainRoute) : RecordingIntent data class ToggleSensor(val sensorId: Int) : RecordingIntent - data class ToggleWearSensor(val sensorId: Int) : RecordingIntent - data class OpenSensorDetails( - val sensorType: Int?, - val source: RecordingSensorSource = RecordingSensorSource.PHONE, - ) : RecordingIntent + data class ToggleWatchSensor(val sensorId: Int) : RecordingIntent + data class OpenSensorDetails(val sensorType: Int?, val device: RecordingDevice = RecordingDevice.PHONE) : + RecordingIntent data object ToggleGps : RecordingIntent - data object ToggleWearGps : RecordingIntent - data object ChooseStorage : RecordingIntent - data object OpenMeasurementSetup : RecordingIntent + data object ToggleWatchGps : RecordingIntent + data object ChooseRecordingArchive : RecordingIntent + data object OpenRecordingSetup : RecordingIntent data object ReturnToSensorSelection : RecordingIntent - data object StartMeasurement : RecordingIntent - data object StopMeasurement : RecordingIntent + data object StartRecording : RecordingIntent + data object StopRecording : RecordingIntent data object ClearMessage : RecordingIntent data class SetCustomMeasurementName(val value: String) : RecordingIntent data class SetStartDelay(val seconds: Int) : RecordingIntent @@ -91,10 +89,10 @@ sealed interface RecordingIntent { data class SetAlarmOffsets(val value: String) : RecordingIntent data class SetActivityRecognition(val enabled: Boolean) : RecordingIntent data class SetActivityRecognitionPeriod(val seconds: Int) : RecordingIntent - data class SetSignificantMotion(val enabled: Boolean) : RecordingIntent + data class SetSignificantMotionRecording(val enabled: Boolean) : RecordingIntent data class AddAnnotation(val text: String) : RecordingIntent data class SetSamplingPeriod(val index: Int) : RecordingIntent - data class SetLowBatteryRestriction(val enabled: Boolean) : RecordingIntent + data class SetStopOnLowBattery(val enabled: Boolean) : RecordingIntent data class SetWakeLock(val enabled: Boolean) : RecordingIntent data class SetKeepScreenAwake(val enabled: Boolean) : RecordingIntent data class SetGpsInterval(val seconds: Int) : RecordingIntent @@ -102,7 +100,7 @@ sealed interface RecordingIntent { } sealed interface RecordingEffect { - data object PickStorageDirectory : RecordingEffect + data object PickRecordingArchive : RecordingEffect data class RequestPermissions(val permissions: Set) : RecordingEffect data class Navigate(val route: MainRoute) : RecordingEffect } @@ -114,45 +112,45 @@ object RecordingReducer { ?: reduceSelection(state, intent) ?: reduceConfiguration(state, intent) - fun measurementStartRequested(state: RecordingState, countdownSeconds: Int): RecordingState = state.copy( + fun recordingStartRequested(state: RecordingState, countdownSeconds: Int): RecordingState = state.copy( isStarting = true, startCountdownSeconds = countdownSeconds.takeIf { it > 0 }, message = RecordingMessage.NONE, errorCode = null, ) - fun measurementCountdownChanged(state: RecordingState, seconds: Int?): RecordingState = state.copy( + fun recordingCountdownChanged(state: RecordingState, seconds: Int?): RecordingState = state.copy( startCountdownSeconds = seconds, ) - fun measurementStartFailed(state: RecordingState): RecordingState = state.copy( + fun recordingStartFailed(state: RecordingState): RecordingState = state.copy( isStarting = false, startCountdownSeconds = null, ) - fun measurementSessionChanged(state: RecordingState, session: MeasurementSessionState): RecordingState = state.copy( + fun recordingSessionChanged(state: RecordingState, session: RecordingSessionState): RecordingState = state.copy( session = session, elapsedSeconds = 0, - isStarting = if (session is MeasurementSessionState.Running) false else state.isStarting, - startCountdownSeconds = if (session is MeasurementSessionState.Running) null else state.startCountdownSeconds, + isStarting = if (session is RecordingSessionState.Running) false else state.isStarting, + startCountdownSeconds = if (session is RecordingSessionState.Running) null else state.startCountdownSeconds, ) private fun reduceNavigation(state: RecordingState, intent: RecordingIntent): RecordingNext? = when (intent) { - RecordingIntent.ChooseStorage -> RecordingNext(state, RecordingEffect.PickStorageDirectory) + RecordingIntent.ChooseRecordingArchive -> RecordingNext(state, RecordingEffect.PickRecordingArchive) is RecordingIntent.Navigate -> RecordingNext(state, RecordingEffect.Navigate(intent.route)) is RecordingIntent.OpenSensorDetails -> RecordingNext( state.copy( detailsSensorType = intent.sensorType, - detailsSensorSource = intent.source, + detailsDevice = intent.device, ), RecordingEffect.Navigate(MainRoute.SENSOR_DETAILS), ) - RecordingIntent.OpenMeasurementSetup -> RecordingNext( + RecordingIntent.OpenRecordingSetup -> RecordingNext( state.copy(message = RecordingMessage.NONE), - RecordingEffect.Navigate(MainRoute.SETUP), + RecordingEffect.Navigate(MainRoute.RECORDING_SETUP), ) RecordingIntent.ReturnToSensorSelection -> RecordingNext( @@ -166,9 +164,9 @@ object RecordingReducer { private fun reduceSelection(state: RecordingState, intent: RecordingIntent): RecordingNext? = when (intent) { RecordingIntent.ClearMessage -> RecordingNext(state.copy(message = RecordingMessage.NONE, errorCode = null)) RecordingIntent.ToggleGps -> RecordingNext(state.copy(includesGps = !state.includesGps)) - RecordingIntent.ToggleWearGps -> RecordingNext(state.copy(wearIncludesGps = !state.wearIncludesGps)) + RecordingIntent.ToggleWatchGps -> RecordingNext(state.copy(watchIncludesGps = !state.watchIncludesGps)) is RecordingIntent.ToggleSensor -> RecordingNext(state.toggleSensor(intent.sensorId)) - is RecordingIntent.ToggleWearSensor -> RecordingNext(state.toggleWearSensor(intent.sensorId)) + is RecordingIntent.ToggleWatchSensor -> RecordingNext(state.toggleWatchSensor(intent.sensorId)) else -> null } @@ -191,7 +189,9 @@ object RecordingReducer { state.copy(activityRecognitionPeriodSeconds = intent.seconds.coerceAtLeast(1)), ) - is RecordingIntent.SetSignificantMotion -> RecordingNext(state.copy(significantMotion = intent.enabled)) + is RecordingIntent.SetSignificantMotionRecording -> RecordingNext( + state.copy(significantMotion = intent.enabled), + ) else -> RecordingNext(state) } @@ -202,9 +202,9 @@ object RecordingReducer { return copy(selectedSensorIds = updated, message = RecordingMessage.NONE, errorCode = null) } - private fun RecordingState.toggleWearSensor(sensorId: Int): RecordingState { - val updated = selectedWearSensorIds.toMutableSet() + private fun RecordingState.toggleWatchSensor(sensorId: Int): RecordingState { + val updated = selectedWatchSensorIds.toMutableSet() if (!updated.add(sensorId)) updated.remove(sensorId) - return copy(selectedWearSensorIds = updated, message = RecordingMessage.NONE, errorCode = null) + return copy(selectedWatchSensorIds = updated, message = RecordingMessage.NONE, errorCode = null) } } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreen.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreen.kt similarity index 87% rename from app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreen.kt rename to app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreen.kt index be15281..ade1f46 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/MeasurementSetupScreen.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingSetupScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.unit.dp import com.tomasrepcik.sensorbox.R @Composable -fun MeasurementSetupScreen( +fun RecordingSetupScreen( state: RecordingState, onIntent: (RecordingIntent) -> Unit, modifier: Modifier = Modifier, @@ -30,22 +30,22 @@ fun MeasurementSetupScreen( ) { Column(modifier.fillMaxSize()) { Box(Modifier.weight(1f)) { - MeasurementSetupContent(state, onIntent, onBack) + RecordingSetupContent(state, onIntent, onBack) } - MeasurementSetupActionBar(state, onIntent) + RecordingSetupTopBar(state, onIntent) } } @Composable -private fun MeasurementSetupContent(state: RecordingState, onIntent: (RecordingIntent) -> Unit, onBack: () -> Unit) { +private fun RecordingSetupContent(state: RecordingState, onIntent: (RecordingIntent) -> Unit, onBack: () -> Unit) { SensorBoxBackScreen( - title = stringResource(R.string.measurement_setup), + title = stringResource(R.string.recording_setup), onBack = onBack, bottomPadding = 24.dp, itemSpacing = 0.dp, ) { item { SensorBoxSettingsSection(stringResource(R.string.setup_storage_category)) } - item { StorageSetupPanel(state.storagePath) { onIntent(RecordingIntent.ChooseStorage) } } + item { RecordingArchivePanel(state.recordingArchivePath) { onIntent(RecordingIntent.ChooseRecordingArchive) } } item { SensorBoxSettingsSection(stringResource(R.string.setup_details_category)) } item { MeasurementNameSetup(state, onIntent) } item { NotesAndAlarmsSetup(state, onIntent) } @@ -95,7 +95,7 @@ private fun RecordingTimingSetup(state: RecordingState, onIntent: (RecordingInte 86_400, ) { onIntent(RecordingIntent.SetStartDelay(it)) } StepSetting( - stringResource(R.string.measurement_duration), + stringResource(R.string.recording_duration), state.durationSeconds, pluralStringResource(R.plurals.seconds_count, state.durationSeconds, state.durationSeconds), 0, @@ -110,8 +110,8 @@ private fun NotesAndAlarmsSetup(state: RecordingState, onIntent: (RecordingInten value = state.notes, onValueChange = { onIntent(RecordingIntent.SetNotes(it)) }, modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), - label = { Text(stringResource(R.string.measurement_notes)) }, - supportingText = { Text(stringResource(R.string.measurement_notes_description)) }, + label = { Text(stringResource(R.string.recording_notes)) }, + supportingText = { Text(stringResource(R.string.recording_notes_description)) }, minLines = 2, ) SensorBoxSettingsDivider() @@ -151,20 +151,20 @@ private fun SpecializedSourcesSetup(state: RecordingState, onIntent: (RecordingI title = stringResource(R.string.significant_motion), description = stringResource(R.string.significant_motion_description), checked = state.significantMotion, - ) { onIntent(RecordingIntent.SetSignificantMotion(it)) } + ) { onIntent(RecordingIntent.SetSignificantMotionRecording(it)) } } } @Composable -private fun StorageSetupPanel(path: String?, onChoose: () -> Unit) { +private fun RecordingArchivePanel(path: String?, onChoose: () -> Unit) { Row( Modifier.fillMaxWidth().padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { - Text(stringResource(R.string.recording_folder), style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.recording_archive), style = MaterialTheme.typography.titleMedium) Text( - path ?: stringResource(R.string.choose_recording_folder), + path ?: stringResource(R.string.choose_recording_archive), color = MaterialTheme.colorScheme.onSurfaceVariant, ) } @@ -182,8 +182,8 @@ private fun BatterySetup(state: RecordingState, onIntent: (RecordingIntent) -> U BooleanSetting( title = stringResource(R.string.battery_guard), description = stringResource(R.string.battery_guard_setup_description), - checked = state.preferences.recording.restrictMeasurementOnLowBattery, - ) { onIntent(RecordingIntent.SetLowBatteryRestriction(it)) } + checked = state.preferences.recording.stopRecordingOnLowBattery, + ) { onIntent(RecordingIntent.SetStopOnLowBattery(it)) } } @Composable @@ -239,7 +239,7 @@ private fun GpsDistanceSetup(state: RecordingState, onIntent: (RecordingIntent) } @Composable -private fun MeasurementSetupActionBar( +private fun RecordingSetupTopBar( state: RecordingState, onIntent: (RecordingIntent) -> Unit, modifier: Modifier = Modifier, @@ -258,11 +258,11 @@ private fun MeasurementSetupActionBar( label = state.startCountdownSeconds?.let { seconds -> pluralStringResource(R.plurals.starting_in_seconds, seconds, seconds) } ?: stringResource( - if (state.isStarting) R.string.starting_recording else R.string.start_measurement, + if (state.isStarting) R.string.starting_recording else R.string.start_recording, ), - onClick = { onIntent(RecordingIntent.StartMeasurement) }, + onClick = { onIntent(RecordingIntent.StartRecording) }, modifier = Modifier.fillMaxWidth(), - enabled = state.storagePath != null && sourceCount > 0 && !state.isStarting, + enabled = state.recordingArchivePath != null && sourceCount > 0 && !state.isStarting, loading = state.isStarting, ) } @@ -270,6 +270,6 @@ private fun MeasurementSetupActionBar( } private fun setupSourceCount(state: RecordingState): Int = - state.selectedSensorIds.size + state.selectedWearSensorIds.size + - (if (state.includesGps) 1 else 0) + (if (state.wearIncludesGps) 1 else 0) + + state.selectedSensorIds.size + state.selectedWatchSensorIds.size + + (if (state.includesGps) 1 else 0) + (if (state.watchIncludesGps) 1 else 0) + (if (state.activityRecognition) 1 else 0) + (if (state.significantMotion) 1 else 0) diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingViewModel.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingViewModel.kt index b1510b6..534c40a 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingViewModel.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordingViewModel.kt @@ -8,15 +8,15 @@ import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesIntent import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesRepository -import com.tomasrepcik.sensorbox.domain.measurement.DocumentStorageGateway -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementPermissionsUseCase -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementRequest -import com.tomasrepcik.sensorbox.domain.measurement.RecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingArchiveRepository +import com.tomasrepcik.sensorbox.domain.recording.RecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingPermissionsUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingSetup import com.tomasrepcik.sensorbox.domain.sensors.AvailableSensorsUseCase -import com.tomasrepcik.sensorbox.domain.sensors.WearSensorCatalogStore +import com.tomasrepcik.sensorbox.domain.sensors.WatchSensorCatalogStore import com.tomasrepcik.sensorbox.domain.sensors.toSensorDescriptor -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionStore +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStore import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_APP_CAPABILITY import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_MESSAGE_PATH import com.tomasrepcik.sensorbox.wearoslib.connectivity.ObserveWearCapabilityUseCase @@ -42,13 +42,13 @@ import javax.inject.Inject class RecordingViewModel @Inject constructor( private val preferencesRepository: AppPreferencesRepository, private val availableSensors: AvailableSensorsUseCase, - private val storage: DocumentStorageGateway, - private val permissions: MeasurementPermissionsUseCase, + private val recordingArchive: RecordingArchiveRepository, + private val permissions: RecordingPermissionsUseCase, private val recording: RecordingControlUseCase, - private val sessionStore: MeasurementSessionStore, - private val observeWearCapability: ObserveWearCapabilityUseCase, - private val sendWearCommand: SendWearCommandUseCase, - private val wearSensorCatalog: WearSensorCatalogStore, + private val sessionStore: RecordingSessionStore, + private val observeWatchCapability: ObserveWearCapabilityUseCase, + private val sendWatchCommand: SendWearCommandUseCase, + private val watchSensorCatalog: WatchSensorCatalogStore, private val elapsedRealtimeClock: ElapsedRealtimeClock, ) : ViewModel() { private val mutableState = MutableStateFlow(initialState()) @@ -61,16 +61,16 @@ class RecordingViewModel @Inject constructor( init { observePreferences() - observeMeasurementSession() - observeWearConnection() - observeWearSensors() + observeRecordingSession() + observeWatchConnection() + observeWatchSensors() } fun accept(intent: RecordingIntent) { when (intent) { - RecordingIntent.StartMeasurement -> startMeasurement() + RecordingIntent.StartRecording -> startRecording() - RecordingIntent.StopMeasurement -> stopMeasurement() + RecordingIntent.StopRecording -> stopRecording() is RecordingIntent.AddAnnotation -> recording.annotate(intent.text).showFailure() @@ -78,8 +78,8 @@ class RecordingViewModel @Inject constructor( AppPreferencesIntent.SetSensorSamplingPeriod(intent.index), ) - is RecordingIntent.SetLowBatteryRestriction -> updatePreference( - AppPreferencesIntent.SetLowBatteryRestriction(intent.enabled), + is RecordingIntent.SetStopOnLowBattery -> updatePreference( + AppPreferencesIntent.SetStopOnLowBattery(intent.enabled), ) is RecordingIntent.SetWakeLock -> updatePreference(AppPreferencesIntent.SetWakeLock(intent.enabled)) @@ -96,22 +96,22 @@ class RecordingViewModel @Inject constructor( } } - fun handleStorageResult(resultIntent: Intent?) { - val persisted = resultIntent?.let(storage::persist) ?: AppResult.failure( - AppError(AppErrorCode.STORAGE, "Select recording storage directory"), + fun handleRecordingArchiveResult(resultIntent: Intent?) { + val persisted = resultIntent?.let(recordingArchive::select) ?: AppResult.failure( + AppError(AppErrorCode.STORAGE, "Select recording archive"), ) mutableState.value = state.value.copy( - storagePath = storage.displayPath().getOrNull(), - message = if (persisted.isSuccess) RecordingMessage.NONE else RecordingMessage.STORAGE_REQUIRED, + recordingArchivePath = recordingArchive.path().getOrNull(), + message = if (persisted.isSuccess) RecordingMessage.NONE else RecordingMessage.RECORDING_ARCHIVE_REQUIRED, errorCode = persisted.errorOrNull()?.code, ) } fun handlePermissionResult() { - val request = state.value.toMeasurementRequest() + val request = state.value.toRecordingSetup() val missing = permissions.missingPermissions(request) if (missing.isEmpty()) { - startMeasurement() + startRecording() } else { showMessage(RecordingMessage.PERMISSION_REQUIRED, AppErrorCode.PERMISSION) } @@ -119,7 +119,7 @@ class RecordingViewModel @Inject constructor( private fun initialState() = RecordingState( sensors = availableSensors(), - storagePath = storage.displayPath().getOrNull(), + recordingArchivePath = recordingArchive.path().getOrNull(), ) private fun observePreferences() { @@ -134,7 +134,7 @@ class RecordingViewModel @Inject constructor( }, onFailure = { error -> mutableState.value = state.value.copy( - message = RecordingMessage.MEASUREMENT_FAILED, + message = RecordingMessage.RECORDING_FAILED, errorCode = error.code, ) }, @@ -143,7 +143,7 @@ class RecordingViewModel @Inject constructor( } } - private fun observeMeasurementSession() { + private fun observeRecordingSession() { viewModelScope.launch { sessionStore.state.collect { session -> onSessionChanged(session) @@ -151,10 +151,10 @@ class RecordingViewModel @Inject constructor( } } - private fun onSessionChanged(session: MeasurementSessionState) { + private fun onSessionChanged(session: RecordingSessionState) { elapsedJob?.cancel() - mutableState.value = RecordingReducer.measurementSessionChanged(state.value, session) - if (session is MeasurementSessionState.Running) startElapsedTicker(session.startedAtElapsedRealtime) + mutableState.value = RecordingReducer.recordingSessionChanged(state.value, session) + if (session is RecordingSessionState.Running) startElapsedTicker(session.startedAtElapsedRealtime) } private fun startElapsedTicker(startedAt: Long) { @@ -167,35 +167,35 @@ class RecordingViewModel @Inject constructor( } } - private fun observeWearConnection() { + private fun observeWatchConnection() { viewModelScope.launch { - observeWearCapability(WEAR_APP_CAPABILITY) + observeWatchCapability(WEAR_APP_CAPABILITY) .catch { error -> AppError.from(AppErrorCode.CONNECTIVITY, "Observe Wear connection", error) emit(WearConnection.Disconnected) } .collect { connection -> if (connection is WearConnection.Connected) { - sendWearCommand( + sendWatchCommand( WEAR_APP_CAPABILITY, WEAR_MESSAGE_PATH, WearCommand.RequestAvailableSensors, ) } else { - wearSensorCatalog.clear() + watchSensorCatalog.clear() } } } } - private fun observeWearSensors() { + private fun observeWatchSensors() { viewModelScope.launch { - combine(wearSensorCatalog.sensors, wearSensorCatalog.isAvailable) { sensors, isAvailable -> + combine(watchSensorCatalog.sensors, watchSensorCatalog.isAvailable) { sensors, isAvailable -> sensors to isAvailable }.collect { (sensors, isAvailable) -> mutableState.value = state.value.copy( - isWearConnected = isAvailable, - wearSensors = sensors.map { sensor -> sensor.toSensorDescriptor() }, + isWatchConnected = isAvailable, + watchSensors = sensors.map { sensor -> sensor.toSensorDescriptor() }, ) } } @@ -208,16 +208,16 @@ class RecordingViewModel @Inject constructor( } @Suppress("ReturnCount") - private fun startMeasurement() { + private fun startRecording() { if (state.value.isStarting) return - val request = state.value.toMeasurementRequest() + val request = state.value.toRecordingSetup() if (!request.hasAnySource()) { showMessage(RecordingMessage.PICK_AT_LEAST_ONE_SOURCE, AppErrorCode.VALIDATION) return } - if (storage.hasStorage().getOrNull() != true) { - reduce(RecordingIntent.ChooseStorage) - showMessage(RecordingMessage.STORAGE_REQUIRED, AppErrorCode.STORAGE) + if (recordingArchive.isSelected().getOrNull() != true) { + reduce(RecordingIntent.ChooseRecordingArchive) + showMessage(RecordingMessage.RECORDING_ARCHIVE_REQUIRED, AppErrorCode.STORAGE) return } requestMissingPermissions(request)?.let { @@ -225,21 +225,21 @@ class RecordingViewModel @Inject constructor( mutableState.value = state.value.copy(errorCode = AppErrorCode.PERMISSION) return } - startForegroundMeasurement(request, state.value.startDelaySeconds) + startRecordingAfterDelay(request, state.value.startDelaySeconds) } - private fun requestMissingPermissions(request: MeasurementRequest): Set? { + private fun requestMissingPermissions(request: RecordingSetup): Set? { val missingPermissions = permissions.missingPermissions(request) return missingPermissions.takeIf(Set::isNotEmpty) } - private fun startForegroundMeasurement(request: MeasurementRequest, countdownSeconds: Int) { - mutableState.value = RecordingReducer.measurementStartRequested(state.value, countdownSeconds) + private fun startRecordingAfterDelay(request: RecordingSetup, countdownSeconds: Int) { + mutableState.value = RecordingReducer.recordingStartRequested(state.value, countdownSeconds) startJob = viewModelScope.launch { waitForStartCountdown(countdownSeconds) val result = recording.start(request) if (result.isFailure) { - mutableState.value = RecordingReducer.measurementStartFailed(state.value) + mutableState.value = RecordingReducer.recordingStartFailed(state.value) showFailure(result.errorOrNull()) } } @@ -247,17 +247,17 @@ class RecordingViewModel @Inject constructor( private suspend fun waitForStartCountdown(seconds: Int) { for (remaining in seconds downTo 1) { - mutableState.value = RecordingReducer.measurementCountdownChanged(state.value, remaining) + mutableState.value = RecordingReducer.recordingCountdownChanged(state.value, remaining) delay(1_000L) } - mutableState.value = RecordingReducer.measurementCountdownChanged(state.value, null) + mutableState.value = RecordingReducer.recordingCountdownChanged(state.value, null) } - private fun stopMeasurement() { - if (state.value.isStarting && state.value.session is MeasurementSessionState.Idle) { + private fun stopRecording() { + if (state.value.isStarting && state.value.session is RecordingSessionState.Idle) { startJob?.cancel() startJob = null - mutableState.value = RecordingReducer.measurementStartFailed(state.value) + mutableState.value = RecordingReducer.recordingStartFailed(state.value) return } viewModelScope.launch { @@ -285,17 +285,17 @@ class RecordingViewModel @Inject constructor( private fun showFailure(error: AppError?) { mutableState.value = state.value.copy( message = when { - error?.code == AppErrorCode.PERMISSION && error.context["source"] == "wear" -> - RecordingMessage.WEAR_PERMISSION_REQUIRED + error?.code == AppErrorCode.PERMISSION && error.context["source"] == "watch" -> + RecordingMessage.WATCH_PERMISSION_REQUIRED error?.code == AppErrorCode.PERMISSION -> RecordingMessage.PERMISSION_REQUIRED - else -> RecordingMessage.MEASUREMENT_FAILED + else -> RecordingMessage.RECORDING_FAILED }, errorCode = error?.code ?: AppErrorCode.UNKNOWN, ) } - private fun MeasurementRequest.hasAnySource(): Boolean = sensorIds.isNotEmpty() || includesGps || - wearSensorIds.isNotEmpty() || wearIncludesGps || activityRecognition || significantMotion + private fun RecordingSetup.hasAnySource(): Boolean = sensorIds.isNotEmpty() || includesGps || + watchSensorIds.isNotEmpty() || watchIncludesGps || activityRecognition || significantMotion } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxApp.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxApp.kt index 732cddb..2aa05f3 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxApp.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxApp.kt @@ -31,7 +31,7 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState @Composable fun SensorBoxApp( @@ -50,7 +50,7 @@ fun SensorBoxApp( Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) return } - if (mainState.keepScreenAwake && mainState.session is MeasurementSessionState.Running) KeepScreenAwake() + if (mainState.keepScreenAwake && mainState.session is RecordingSessionState.Running) KeepScreenAwake() SensorBoxNavHost( mainState = mainState, onboardingState = onboardingState, @@ -108,8 +108,8 @@ private fun SensorBoxNavHost( ) } -private fun MainState.displayedRoute() = if (session is MeasurementSessionState.Running) { - MainRoute.ACTIVE_MEASUREMENT +private fun MainState.displayedRoute() = if (session is RecordingSessionState.Running) { + MainRoute.ACTIVE_RECORDING } else { route } @@ -117,7 +117,7 @@ private fun MainState.displayedRoute() = if (session is MeasurementSessionState. @Composable private fun SynchronizeBackStack(backStack: NavBackStack, displayedRoute: MainRoute) { LaunchedEffect(displayedRoute) { - if (displayedRoute != MainRoute.ACTIVE_MEASUREMENT && backStack.lastOrNull() == MainRoute.ACTIVE_MEASUREMENT) { + if (displayedRoute != MainRoute.ACTIVE_RECORDING && backStack.lastOrNull() == MainRoute.ACTIVE_RECORDING) { backStack.removeLastOrNull() } if (backStack.lastOrNull() != displayedRoute) { @@ -128,10 +128,10 @@ private fun SynchronizeBackStack(backStack: NavBackStack, displayedRoute } private fun navigateBack(state: MainState, backStack: NavBackStack, onNavigate: (MainRoute) -> Unit) { - if (state.session is MeasurementSessionState.Running) return + if (state.session is RecordingSessionState.Running) return if (backStack.size > 1) backStack.removeLastOrNull() val destination = backStack.lastOrNull() as? MainRoute ?: MainRoute.RECORD - if (destination != MainRoute.ACTIVE_MEASUREMENT) onNavigate(destination) + if (destination != MainRoute.ACTIVE_RECORDING) onNavigate(destination) } private fun MainRoute.isRootDestination() = when (this) { @@ -139,10 +139,10 @@ private fun MainRoute.isRootDestination() = when (this) { MainRoute.RECORD, -> true - MainRoute.ACTIVE_MEASUREMENT, + MainRoute.ACTIVE_RECORDING, MainRoute.SENSOR_DETAILS, MainRoute.SENSOR_PREVIEW, - MainRoute.SETUP, + MainRoute.RECORDING_SETUP, MainRoute.MEASUREMENTS, MainRoute.MEASUREMENT_DETAILS, MainRoute.MEASUREMENT_FILE, @@ -169,8 +169,8 @@ private fun RouteContent( when (route) { MainRoute.ONBOARDING -> OnboardingScreen(onboardingState, onOnboardingIntent) - MainRoute.ACTIVE_MEASUREMENT -> FullScreen { modifier -> - ActiveMeasurementScreen(recordingState, onRecordingIntent, modifier) + MainRoute.ACTIVE_RECORDING -> FullScreen { modifier -> + ActiveRecordingScreen(recordingState, onRecordingIntent, modifier) } MainRoute.SENSOR_DETAILS -> FullScreen { modifier -> @@ -186,8 +186,8 @@ private fun RouteContent( SensorPreviewScreen(state = recordingState, onBack = onBack, modifier = modifier) } - MainRoute.SETUP -> FullScreen { modifier -> - MeasurementSetupScreen( + MainRoute.RECORDING_SETUP -> FullScreen { modifier -> + RecordingSetupScreen( state = recordingState, onIntent = onRecordingIntent, modifier = modifier, @@ -242,7 +242,7 @@ private fun MeasurementBrowserRoute( MainRoute.MEASUREMENTS -> MeasurementsScreen(state, onIntent, onBack, modifier) MainRoute.MEASUREMENT_DETAILS -> MeasurementDetailsScreen(state, onIntent, onBack, modifier) MainRoute.MEASUREMENT_FILE -> MeasurementFileScreen(state, onIntent, onBack, modifier) - else -> error("Not a measurement archive route") + else -> error("Not a measurement browser route") } } } diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxComponents.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxComponents.kt index 2a440ab..db492b1 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxComponents.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorBoxComponents.kt @@ -216,7 +216,7 @@ fun SensorBoxSecondaryButton( } @Composable -fun SensorBoxBottomAction( +fun SensorBoxBottomButton( title: String, description: String, buttonLabel: String, diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreen.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreen.kt index 2f4c465..93c981f 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreen.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorDetailsScreen.kt @@ -47,9 +47,9 @@ fun SensorDetailsScreen( modifier: Modifier = Modifier, ) { val sensor = state.detailsSensorType?.let { type -> - val sensors = when (state.detailsSensorSource) { - RecordingSensorSource.PHONE -> state.sensors - RecordingSensorSource.WEAR -> state.wearSensors + val sensors = when (state.detailsDevice) { + RecordingDevice.PHONE -> state.sensors + RecordingDevice.WATCH -> state.watchSensors } sensors.firstOrNull { it.type == type } } @@ -59,12 +59,12 @@ fun SensorDetailsScreen( sensor != null -> sensor.name else -> stringResource(R.string.sensor_unavailable) } - val showPreview = state.detailsSensorSource == RecordingSensorSource.PHONE && + val showPreview = state.detailsDevice == RecordingDevice.PHONE && (state.detailsSensorType == null || sensor != null) && canPreview Box(modifier.fillMaxSize()) { SensorDetailsList(state, sensor, title, showPreview, onBack) if (showPreview) { - PreviewAction(onPreview, Modifier.align(Alignment.BottomCenter)) + PreviewButton(onPreview, Modifier.align(Alignment.BottomCenter)) } } } @@ -142,7 +142,7 @@ private fun GpsDetails(state: RecordingState) { GpsPermissionStatus(details.hasPermission) GpsDetailRows(details, state, unavailableValue) if (!details.hasPermission) { - GpsPermissionAction { + GpsPermissionButton { permissionRequest.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, @@ -195,7 +195,7 @@ private fun localizedValue(value: Number?, unavailableValue: String): String = @Composable internal fun GpsPermission(hasPermission: Boolean, onRequest: () -> Unit) { GpsPermissionStatus(hasPermission) - if (!hasPermission) GpsPermissionAction(onRequest) + if (!hasPermission) GpsPermissionButton(onRequest) } @Composable @@ -207,7 +207,7 @@ private fun GpsPermissionStatus(hasPermission: Boolean) { } @Composable -private fun GpsPermissionAction(onRequest: () -> Unit) { +private fun GpsPermissionButton(onRequest: () -> Unit) { Text( stringResource(R.string.location_permission_explanation), color = MaterialTheme.colorScheme.error, @@ -229,7 +229,7 @@ private fun ParameterRow(label: String, value: String, showDivider: Boolean = tr } @Composable -private fun PreviewAction(onPreview: () -> Unit, modifier: Modifier = Modifier) { +private fun PreviewButton(onPreview: () -> Unit, modifier: Modifier = Modifier) { Surface(modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.background) { Box( Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 14.dp), diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorPreviewScreen.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorPreviewScreen.kt index 0ff584d..16cb86f 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorPreviewScreen.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SensorPreviewScreen.kt @@ -50,7 +50,7 @@ import kotlin.math.max @Composable fun SensorPreviewScreen(state: RecordingState, onBack: () -> Unit, modifier: Modifier = Modifier) { - if (state.detailsSensorSource == RecordingSensorSource.WEAR) { + if (state.detailsDevice == RecordingDevice.WATCH) { SensorBoxBackScreen( title = stringResource(R.string.sensor_unavailable), onBack = onBack, diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsContract.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsContract.kt index a951f4f..56aecc4 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsContract.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsContract.kt @@ -13,7 +13,7 @@ data class SettingsState( sealed interface SettingsIntent { data class SetSamplingPeriod(val index: Int) : SettingsIntent - data class SetLowBatteryRestriction(val enabled: Boolean) : SettingsIntent + data class SetStopOnLowBattery(val enabled: Boolean) : SettingsIntent data class SetWakeLock(val enabled: Boolean) : SettingsIntent data class SetKeepScreenAwake(val enabled: Boolean) : SettingsIntent data class SetGpsInterval(val seconds: Int) : SettingsIntent diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsScreen.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsScreen.kt index 75237eb..8e1e8ed 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsScreen.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsScreen.kt @@ -58,7 +58,7 @@ fun SettingsScreen( val isBatteryOptimizationExempt = rememberBatteryOptimizationExemption() LaunchedEffect(Unit) { onIntent(SettingsIntent.ViewDiagnostics) } SensorBoxBackScreen( - title = stringResource(R.string.measurement_settings), + title = stringResource(R.string.recording_settings), onBack = onBack, modifier = modifier, itemSpacing = 0.dp, @@ -121,15 +121,15 @@ private fun DiagnosticsSetting(state: SettingsState, onIntent: (SettingsIntent) color = MaterialTheme.colorScheme.onSurfaceVariant, ) } else { - SettingsActionRow( + SettingsControlRow( title = stringResource(R.string.diagnostics_view), description = stringResource(R.string.diagnostics_view_summary), ) { onIntent(SettingsIntent.Navigate(MainRoute.DIAGNOSTICS)) } - SettingsActionRow( + SettingsControlRow( title = stringResource(R.string.diagnostics_share), description = stringResource(R.string.diagnostics_share_summary), ) { onIntent(SettingsIntent.ShareDiagnosticsFile) } - SettingsActionRow( + SettingsControlRow( title = stringResource(R.string.diagnostics_clear), description = stringResource(R.string.diagnostics_clear_summary), showDivider = false, @@ -259,8 +259,8 @@ private fun BatteryGuardSetting(state: SettingsState, onIntent: (SettingsIntent) BooleanSetting( title = stringResource(R.string.battery_guard), description = stringResource(R.string.battery_guard_settings_description), - checked = state.preferences.recording.restrictMeasurementOnLowBattery, - ) { onIntent(SettingsIntent.SetLowBatteryRestriction(it)) } + checked = state.preferences.recording.stopRecordingOnLowBattery, + ) { onIntent(SettingsIntent.SetStopOnLowBattery(it)) } } @Composable @@ -306,7 +306,7 @@ private fun BatteryOptimizationSetting(isExempt: Boolean, onIntent: (SettingsInt private fun AboutSetting(onIntent: (SettingsIntent) -> Unit) { var showAboutDialog by rememberSaveable { mutableStateOf(false) } - SettingsActionRow( + SettingsControlRow( title = stringResource(R.string.menu_about), description = stringResource(R.string.about_summary), showDivider = false, @@ -411,7 +411,7 @@ fun BooleanSetting(title: String, description: String, checked: Boolean, onCheck } @Composable -private fun SettingsActionRow( +private fun SettingsControlRow( title: String, description: String, showDivider: Boolean = true, diff --git a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsViewModel.kt b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsViewModel.kt index 9002d9a..f86f7be 100644 --- a/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsViewModel.kt +++ b/app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/SettingsViewModel.kt @@ -69,7 +69,7 @@ class SettingsViewModel @Inject constructor( is SettingsIntent.Navigate -> mutableEffects.trySend(SettingsEffect.Navigate(intent.route)) is SettingsIntent.SetSamplingPeriod, - is SettingsIntent.SetLowBatteryRestriction, + is SettingsIntent.SetStopOnLowBattery, is SettingsIntent.SetWakeLock, is SettingsIntent.SetKeepScreenAwake, is SettingsIntent.SetGpsInterval, @@ -82,7 +82,7 @@ class SettingsViewModel @Inject constructor( private fun SettingsIntent.toPreferencesIntent(): AppPreferencesIntent? = when (this) { is SettingsIntent.SetSamplingPeriod -> AppPreferencesIntent.SetSensorSamplingPeriod(index) - is SettingsIntent.SetLowBatteryRestriction -> AppPreferencesIntent.SetLowBatteryRestriction(enabled) + is SettingsIntent.SetStopOnLowBattery -> AppPreferencesIntent.SetStopOnLowBattery(enabled) is SettingsIntent.SetWakeLock -> AppPreferencesIntent.SetWakeLock(enabled) is SettingsIntent.SetKeepScreenAwake -> AppPreferencesIntent.SetKeepPhoneDisplayOn(enabled) is SettingsIntent.SetGpsInterval -> AppPreferencesIntent.SetGpsInterval(seconds) @@ -122,7 +122,7 @@ class SettingsViewModel @Inject constructor( withDiagnostics { mutableEffects.send(SettingsEffect.ShareDiagnosticsFile) } } - private fun withDiagnostics(action: suspend () -> Unit) { + private fun withDiagnostics(operation: suspend () -> Unit) { viewModelScope.launch(ioDispatcher) { when (val result = diagnosticsStore.readText()) { is AppResult.Success -> { @@ -131,7 +131,7 @@ class SettingsViewModel @Inject constructor( diagnosticsLoaded = true, errorCode = null, ) - if (result.value.isNotBlank()) action() + if (result.value.isNotBlank()) operation() } is AppResult.Failure -> fail(result.error.code) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6fd53c5..93a1c96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,7 +29,7 @@ Nothing is going out Measurements stay locally on your devices. SensorBox has no analytics, cloud upload, or account requirement. Privacy and terms - Review the policies before continuing. SensorBox only records when you explicitly start a measurement. + Review the policies before continuing. SensorBox only records when you explicitly start recording. Privacy policy Terms of use Android may pause recordings @@ -37,14 +37,14 @@ Allow reliable background work You can ask Android to exempt SensorBox from battery optimization. This is optional and can be changed later in system settings. Review battery settings - Choose a recording folder - Android’s folder picker gives SensorBox access only to the location you choose. A folder is required before finishing setup. - Choose folder - Change folder - Selected folder: %1$s + Choose a recording archive + Android’s folder picker gives SensorBox access only to the directory you choose as the recording archive. An archive is required before finishing setup. + Choose archive + Change archive + Recording archive: %1$s Back Start SensorBox - SensorBox does not collect or upload your sensor or GPS recordings. Your measurements stay in the folder you select. Recording begins only after your action and remains visible through a foreground-service notification. + SensorBox does not collect or upload your sensor or GPS recordings. Your measurements stay in the recording archive you select. Recording begins only after your intent and remains visible through a foreground-service notification. https://tomasrepcik.dev/sensorbox/terms-of-use https://tomasrepcik.dev/sensorbox/privacy-policy @@ -71,16 +71,16 @@ Privacy and health data Why SensorBox requests access Sensor and location permissions are requested only after you select those sources and tap Start recording. - Recordings are written to the folder you choose through Android’s system document picker. + Measurements are written to the recording archive you choose through Android’s system document picker. SensorBox does not upload recorded sensor or location data. Pick sensors Phone and Wear OS ready - Choose signals for this measurement + Choose recording sources Options Sensors Phone - Wear OS + Wear OS %1$d of %2$d selected Device location Information about GPS @@ -98,14 +98,14 @@ Select a recording source Pick at least one sensor or GPS. - Recording folder required - Choose a folder before recording. + Recording archive required + Choose a recording archive before recording. Permission required Grant the required permission, then try again. Permission required on Wear OS Open SensorBox on your watch, grant Location permission, then try again. - Recording could not start - Check diagnostics for details, then try again. + Recording could not start + Check diagnostics for details, then try again. Measurements Measurement details @@ -122,8 +122,8 @@ GPS coordinates Text data Loading measurements… - The selected recording folder could not be read. - No measurements were found in the selected recording folder. + The recording archive could not be read. + No measurements were found in the recording archive. %1$d chart samples Zoom out Show all @@ -138,15 +138,15 @@ Altitude: %1$s m Accuracy: %1$s m Only the first 100,000 characters are shown. - Measurement settings - Control how measurements are recorded - Measurement setup + Recording settings + Control how recordings run + Recording setup %1$s ready - Recording folder - Choose where recordings are saved - Folder required + Recording archive + Choose the phone directory for measurements + Recording archive required Ready to record - Start measurement + Start recording Starting recording… Starting in %d second… @@ -155,9 +155,9 @@ Measurement name Optional folder-name prefix Start delay - Duration - Notes - One note per line; saved in extra.json + Duration + Notes + One note per line; saved in extra.json Audible alarms Seconds after start, separated by commas Activity recognition @@ -166,7 +166,7 @@ Significant motion Record and re-arm the one-shot trigger sensor Battery guard - Stop a session when the battery becomes low + Stop recording when the battery becomes low Stop when the battery becomes low Keep CPU awake Use a partial wake lock while recording @@ -198,7 +198,7 @@ Details Timing Sources - Markers + Markers Theme Choose how the app follows the light and dark theme Automatic @@ -291,6 +291,6 @@ Delete both retained diagnostic log files? This cannot be undone. Diagnostics copied. Diagnostics cleared. - The diagnostics action failed. + The diagnostics operation failed. Diagnostics could not be shared. The error was saved for a later attempt. diff --git a/app/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/main/ReadmePhoneScreenshotPreviews.kt b/app/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/main/ReadmePhoneScreenshotPreviews.kt index de8dd23..c86670e 100644 --- a/app/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/main/ReadmePhoneScreenshotPreviews.kt +++ b/app/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/main/ReadmePhoneScreenshotPreviews.kt @@ -22,8 +22,8 @@ fun readmeSourceSelection() = PhoneReadmeFrame { @PreviewTest @ReadmePhonePreview @Composable -fun readmeMeasurementSetup() = PhoneReadmeFrame { - MeasurementSetupScreen( +fun readmeRecordingSetup() = PhoneReadmeFrame { + RecordingSetupScreen( state = readmeRecordingState().copy(selectedSensorIds = setOf(Sensor.TYPE_GRAVITY)), onIntent = {}, onBack = {}, @@ -58,11 +58,11 @@ fun readmeIntroBattery() = OnboardingReadmePreview(page = 4) @PreviewTest @ReadmePhonePreview @Composable -fun readmeIntroStorage() = OnboardingReadmePreview(page = 5, storagePath = "Documents/SensorBox") +fun readmeIntroStorage() = OnboardingReadmePreview(page = 5, recordingArchivePath = "Documents/SensorBox") @Composable -private fun OnboardingReadmePreview(page: Int, storagePath: String? = null) = PhoneReadmeFrame { - OnboardingScreen(state = OnboardingState(page = page, storagePath = storagePath), onIntent = {}) +private fun OnboardingReadmePreview(page: Int, recordingArchivePath: String? = null) = PhoneReadmeFrame { + OnboardingScreen(state = OnboardingState(page = page, recordingArchivePath = recordingArchivePath), onIntent = {}) } @Composable @@ -82,7 +82,7 @@ private fun readmeRecordingState() = RecordingState( SensorDescriptor(Sensor.TYPE_MAGNETIC_FIELD, "MMC56X3X Magnetometer", "MEMSIC"), SensorDescriptor(Sensor.TYPE_ROTATION_VECTOR, "Rotation Vector Sensor", "Google"), ), - storagePath = "Documents/SensorBox", + recordingArchivePath = "Documents/SensorBox", ) @Preview( diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinatorTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinatorTest.kt index 484872a..126848c 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinatorTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinatorTest.kt @@ -4,9 +4,9 @@ import com.tomasrepcik.sensorbox.core.error.AppError import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.DiagnosticLogger -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementRequest -import com.tomasrepcik.sensorbox.domain.measurement.PhoneRecordingController -import com.tomasrepcik.sensorbox.domain.measurement.StartedPhoneRecording +import com.tomasrepcik.sensorbox.domain.recording.PhoneRecordingController +import com.tomasrepcik.sensorbox.domain.recording.RecordingSetup +import com.tomasrepcik.sensorbox.domain.recording.StartedPhoneRecording import com.tomasrepcik.sensorbox.wearoslib.connectivity.SendWearMessageUseCase import com.tomasrepcik.sensorbox.wearoslib.connectivity.WearConnection import com.tomasrepcik.sensorbox.wearoslib.connectivity.WearConnectionRepository @@ -14,7 +14,7 @@ import com.tomasrepcik.sensorbox.wearoslib.connectivity.WearNode import com.tomasrepcik.sensorbox.wearoslib.protocol.SendWearCommandUseCase import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommandCodec -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOutcome import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import kotlinx.coroutines.flow.Flow @@ -26,7 +26,7 @@ import org.junit.Test class PairedRecordingCoordinatorTest { @Test - fun `Given phone sources only When recording starts Then no Wear command is sent`() = runTest { + fun `Given phone sources only When recording starts Then no watch command is sent`() = runTest { // Given val fixture = Fixture() @@ -40,12 +40,12 @@ class PairedRecordingCoordinatorTest { } @Test - fun `Given Wear sources When recording starts Then phone starts before direct Wear start`() = runTest { + fun `Given watch sources When recording starts Then phone starts before direct watch start`() = runTest { // Given val fixture = Fixture() // When - val result = fixture.coordinator.start(request(wearSensorIds = setOf(1))) + val result = fixture.coordinator.start(request(watchSensorIds = setOf(1))) // Then assertTrue(result.isSuccess) @@ -60,23 +60,23 @@ class PairedRecordingCoordinatorTest { // When val result = fixture.coordinator.start( - request(wearSensorIds = setOf(1), durationSeconds = 12), + request(watchSensorIds = setOf(1), durationSeconds = 12), ) // Then - val wearStart = fixture.repository.commands.single() as WearCommand.StartRecording + val watchStart = fixture.repository.commands.single() as WearCommand.StartRecording assertTrue(result.isSuccess) assertEquals(12_000L, fixture.phone.startedDurationMillis) - assertEquals(fixture.phone.startedDurationMillis, wearStart.request.durationMillis) + assertEquals(fixture.phone.startedDurationMillis, watchStart.request.durationMillis) } @Test - fun `Given Wear start fails When recording starts Then phone and Wear are stopped`() = runTest { + fun `Given watch start fails When recording starts Then phone and watch are stopped`() = runTest { // Given val fixture = Fixture().apply { repository.failStart = true } // When - val result = fixture.coordinator.start(request(wearSensorIds = setOf(1))) + val result = fixture.coordinator.start(request(watchSensorIds = setOf(1))) // Then assertTrue(result.isFailure) @@ -85,13 +85,13 @@ class PairedRecordingCoordinatorTest { } @Test - fun `Given Wear start result is lost When recording starts Then phone keeps recording`() = runTest { + fun `Given watch start result is lost When recording starts Then phone keeps recording`() = runTest { // Given val fixture = Fixture().apply { repository.dropStartResult = true } // When val result = fixture.coordinator.start( - request(wearSensorIds = setOf(1), durationSeconds = 12), + request(watchSensorIds = setOf(1), durationSeconds = 12), ) // Then @@ -102,10 +102,10 @@ class PairedRecordingCoordinatorTest { } @Test - fun `Given paired recording When stopped Then phone and Wear receive stop`() = runTest { + fun `Given paired recording When stopped Then phone and watch receive stop`() = runTest { // Given val fixture = Fixture() - fixture.coordinator.start(request(wearSensorIds = setOf(1))) + fixture.coordinator.start(request(watchSensorIds = setOf(1))) // When val result = fixture.coordinator.stop() @@ -132,7 +132,7 @@ class PairedRecordingCoordinatorTest { } @Test - fun `Given active recording When stale Wear stop arrives Then phone keeps recording`() = runTest { + fun `Given active recording When stale watch stop arrives Then phone keeps recording`() = runTest { // Given val fixture = Fixture() fixture.coordinator.start(request()) @@ -146,11 +146,11 @@ class PairedRecordingCoordinatorTest { } @Test - fun `Given phone timer expires while Wear is unreachable When recording starts again Then phone is available`() = + fun `Given phone timer expires while watch is unreachable When recording starts again Then phone is available`() = runTest { // Given val fixture = Fixture() - fixture.coordinator.start(request(wearSensorIds = setOf(1), durationSeconds = 12)) + fixture.coordinator.start(request(watchSensorIds = setOf(1), durationSeconds = 12)) val sessionId = (fixture.repository.commands.single() as WearCommand.StartRecording).sessionId fixture.repository.failSends = true @@ -169,7 +169,7 @@ class PairedRecordingCoordinatorTest { assertEquals(2, fixture.phone.startCalls) } - private fun request(wearSensorIds: Set = emptySet(), durationSeconds: Int = 0) = MeasurementRequest( + private fun request(watchSensorIds: Set = emptySet(), durationSeconds: Int = 0) = RecordingSetup( sensorIds = setOf(1), includesGps = false, samplingPeriodIndex = 0, @@ -177,12 +177,12 @@ class PairedRecordingCoordinatorTest { useWakeLock = false, gpsIntervalSeconds = 10, gpsMinDistanceMeters = 20, - wearSensorIds = wearSensorIds, + watchSensorIds = watchSensorIds, durationSeconds = durationSeconds, ) private class Fixture { - val inbox = WearRecordingResultInbox() + val inbox = WatchRecordingResultInbox() val phone = FakePhoneRecordingController() val repository = RecordingResultRepository(inbox) val coordinator = PairedRecordingCoordinator( @@ -199,7 +199,7 @@ private class FakePhoneRecordingController : PhoneRecordingController { var stopCalls = 0 var startedDurationMillis = 0L - override fun start(sessionId: String, request: MeasurementRequest): AppResult { + override fun start(sessionId: String, request: RecordingSetup): AppResult { startCalls += 1 startedDurationMillis = request.durationSeconds.coerceAtLeast(0) * 1_000L return AppResult.success(StartedPhoneRecording(sessionId, "fixture", startedDurationMillis)) @@ -218,7 +218,7 @@ private class FakePhoneRecordingController : PhoneRecordingController { override fun annotate(text: String, timestampMillis: Long): AppResult = AppResult.success(Unit) } -private class RecordingResultRepository(private val inbox: WearRecordingResultInbox) : WearConnectionRepository { +private class RecordingResultRepository(private val inbox: WatchRecordingResultInbox) : WearConnectionRepository { val commands = mutableListOf() var failStart = false var failSends = false @@ -239,26 +239,26 @@ private class RecordingResultRepository(private val inbox: WearRecordingResultIn is WearCommand.StartRecording -> if (!dropStartResult) { publish( command.sessionId, - WearRecordingAction.START, + WearRecordingOperation.START, failed = failStart, ) } - is WearCommand.StopRecording -> publish(command.sessionId, WearRecordingAction.STOP, failed = false) + is WearCommand.StopRecording -> publish(command.sessionId, WearRecordingOperation.STOP, failed = false) else -> Unit } return AppResult.success(Unit) } - private fun publish(sessionId: String, action: WearRecordingAction, failed: Boolean) { + private fun publish(sessionId: String, operation: WearRecordingOperation, failed: Boolean) { inbox.publish( WearCommand.RecordingResult( sessionId = sessionId, - action = action, + operation = operation, outcome = if (failed) WearRecordingOutcome.FAILED else WearRecordingOutcome.SUCCEEDED, - errorCode = AppErrorCode.MEASUREMENT.takeIf { failed }, - errorOperation = "Start Wear recording".takeIf { failed }, + errorCode = AppErrorCode.RECORDING.takeIf { failed }, + errorOperation = "Start watch recording".takeIf { failed }, errorMessage = "Fixture start failed".takeIf { failed }, failureCount = if (failed) 1 else 0, ), diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/DocumentStorageUseCaseTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/DocumentRecordingArchiveRepositoryTest.kt similarity index 76% rename from app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/DocumentStorageUseCaseTest.kt rename to app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/DocumentRecordingArchiveRepositoryTest.kt index 9c5ee7b..f46b004 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/DocumentStorageUseCaseTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/DocumentRecordingArchiveRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.content.Intent import com.tomasrepcik.sensorbox.core.error.AppResult @@ -10,16 +10,16 @@ import org.junit.Test import java.io.InputStream import java.io.OutputStream -class DocumentStorageUseCaseTest { +class DocumentRecordingArchiveRepositoryTest { @Test - fun `Given a document storage adapter When storage operations run Then the same interface owns every result`() { + fun `Given document storage When recording archive operations run Then each result is returned`() { val storage = FakeDocumentStorage() - val useCase = DocumentStorageUseCase(storage) + val repository = DocumentRecordingArchiveRepository(storage) val intent = Intent() - assertTrue(useCase.hasStorage().getOrNull() == true) - assertEquals("Selected folder", useCase.displayPath().getOrNull()) - assertTrue(useCase.persist(intent).isSuccess) + assertTrue(repository.isSelected().getOrNull() == true) + assertEquals("Documents/SensorBox", repository.path().getOrNull()) + assertTrue(repository.select(intent).isSuccess) assertSame(intent, storage.persistedIntent) } @@ -33,7 +33,7 @@ class DocumentStorageUseCaseTest { override fun hasConfiguredDirectory(): AppResult = AppResult.success(true) - override fun displayPath(): AppResult = AppResult.success("Selected folder") + override fun displayPath(): AppResult = AppResult.success("Documents/SensorBox") override fun createMeasurementDirectory(measurementName: String): AppResult = AppResult.success(Unit) diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementPermissionUseCaseTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/RecordingPermissionUseCaseTest.kt similarity index 78% rename from app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementPermissionUseCaseTest.kt rename to app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/RecordingPermissionUseCaseTest.kt index 444fcc1..f6a230a 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementPermissionUseCaseTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/RecordingPermissionUseCaseTest.kt @@ -1,10 +1,10 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.Manifest import org.junit.Assert.assertEquals import org.junit.Test -class MeasurementPermissionUseCaseTest { +class RecordingPermissionUseCaseTest { @Test fun `Given optional phone sources When permissions are derived Then only supported permissions are used`() { val request = request(includesGps = true, activityRecognition = true) @@ -15,7 +15,7 @@ class MeasurementPermissionUseCaseTest { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACTIVITY_RECOGNITION, ), - requiredMeasurementPermissions(request, sdkInt = 37), + requiredRecordingPermissions(request, sdkInt = 37), ) } @@ -23,11 +23,11 @@ class MeasurementPermissionUseCaseTest { fun `Given a legacy phone When permissions are derived Then version gated permissions are absent`() { assertEquals( setOf(Manifest.permission.ACCESS_FINE_LOCATION), - requiredMeasurementPermissions(request(includesGps = true, activityRecognition = true), sdkInt = 28), + requiredRecordingPermissions(request(includesGps = true, activityRecognition = true), sdkInt = 28), ) } - private fun request(includesGps: Boolean, activityRecognition: Boolean) = MeasurementRequest( + private fun request(includesGps: Boolean, activityRecognition: Boolean) = RecordingSetup( sensorIds = emptySet(), includesGps = includesGps, samplingPeriodIndex = 0, diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementRequestTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/RecordingSetupTest.kt similarity index 79% rename from app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementRequestTest.kt rename to app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/RecordingSetupTest.kt index 3534c8c..9abf379 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementRequestTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/domain/recording/RecordingSetupTest.kt @@ -1,13 +1,13 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.hardware.SensorManager import org.junit.Assert.assertEquals import org.junit.Test -class MeasurementRequestTest { +class RecordingSetupTest { @Test - fun `Given a measurement request When converted Then launch configuration is preserved`() { - val request = MeasurementRequest( + fun `Given a recording setup When converted Then recording request is preserved`() { + val request = RecordingSetup( sensorIds = setOf(1, 4), includesGps = true, samplingPeriodIndex = 2, @@ -23,7 +23,7 @@ class MeasurementRequestTest { significantMotion = true, ) - val launch = request.toLaunchRequest("session-1", "folder") + val launch = request.toRecordingRequest("session-1", "folder") assertEquals("session-1", launch.sessionId) assertEquals("folder", launch.folderName) @@ -38,7 +38,7 @@ class MeasurementRequestTest { @Test fun `Given an unknown sampling index When converted Then fastest is used`() { - val request = MeasurementRequest( + val request = RecordingSetup( sensorIds = emptySet(), includesGps = false, samplingPeriodIndex = Int.MAX_VALUE, @@ -50,7 +50,7 @@ class MeasurementRequestTest { assertEquals( SensorManager.SENSOR_DELAY_FASTEST, - request.toLaunchRequest("session-1", "folder").sensorSamplingPeriod, + request.toRecordingRequest("session-1", "folder").sensorSamplingPeriod, ) } } diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorCatalogStoreTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorCatalogStoreTest.kt similarity index 59% rename from app/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorCatalogStoreTest.kt rename to app/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorCatalogStoreTest.kt index 70cbfaa..0053880 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorCatalogStoreTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorCatalogStoreTest.kt @@ -4,10 +4,10 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -class WearSensorCatalogStoreTest { +class WatchSensorCatalogStoreTest { @Test - fun `Given an empty sensor response When catalog updates Then Wear is available`() { - val catalog = WearSensorCatalogStore() + fun `Given an empty sensor response When catalog updates Then watch is available`() { + val catalog = WatchSensorCatalogStore() catalog.update(emptyList()) @@ -15,8 +15,8 @@ class WearSensorCatalogStoreTest { } @Test - fun `Given an available Wear app When catalog clears Then Wear is unavailable`() { - val catalog = WearSensorCatalogStore().apply { update(emptyList()) } + fun `Given an available watch app When catalog clears Then watch is unavailable`() { + val catalog = WatchSensorCatalogStore().apply { update(emptyList()) } catalog.clear() diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/FeatureViewModelTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/FeatureViewModelTest.kt index 9edd339..e7e7b7e 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/FeatureViewModelTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/FeatureViewModelTest.kt @@ -7,15 +7,15 @@ import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.DiagnosticsStore import com.tomasrepcik.sensorbox.core.preferences.AppThemeMode import com.tomasrepcik.sensorbox.core.testing.FakeAppPreferencesRepository -import com.tomasrepcik.sensorbox.domain.measurement.DocumentStorageGateway -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementPermissionsUseCase -import com.tomasrepcik.sensorbox.domain.measurement.MeasurementRequest -import com.tomasrepcik.sensorbox.domain.measurement.RecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingArchiveRepository +import com.tomasrepcik.sensorbox.domain.recording.RecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingPermissionsUseCase +import com.tomasrepcik.sensorbox.domain.recording.RecordingSetup import com.tomasrepcik.sensorbox.domain.sensors.AvailableSensorsUseCase import com.tomasrepcik.sensorbox.domain.sensors.SensorDescriptor -import com.tomasrepcik.sensorbox.domain.sensors.WearSensorCatalogStore -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionStore +import com.tomasrepcik.sensorbox.domain.sensors.WatchSensorCatalogStore +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStore import com.tomasrepcik.sensorbox.testing.MainDispatcherRule import com.tomasrepcik.sensorbox.wearoslib.connectivity.FakeWearConnectionRepository import com.tomasrepcik.sensorbox.wearoslib.connectivity.ObserveWearCapabilityUseCase @@ -43,35 +43,37 @@ class FeatureViewModelTest { val mainDispatcherRule = MainDispatcherRule() @Test - fun `Given onboarding storage When completed Then state and navigation effect are owned by onboarding`() = runTest { - val viewModel = OnboardingViewModel( - preferencesRepository = FakeAppPreferencesRepository(), - documentStorage = FakeDocumentStorage(hasStorage = true), - ) - val effect = async { viewModel.effects.first() } + fun `Given onboarding recording archive When completed Then state and navigation effect are owned by onboarding`() = + runTest { + val viewModel = OnboardingViewModel( + preferencesRepository = FakeAppPreferencesRepository(), + recordingArchive = FakeRecordingArchiveRepository(isSelected = true), + ) + val effect = async { viewModel.effects.first() } - viewModel.accept(OnboardingIntent.AdvanceOnboarding) - viewModel.accept(OnboardingIntent.CompleteOnboarding) - advanceUntilIdle() + viewModel.accept(OnboardingIntent.AdvanceOnboarding) + viewModel.accept(OnboardingIntent.CompleteOnboarding) + advanceUntilIdle() - assertEquals(1, viewModel.state.value.page) - assertEquals(OnboardingEffect.Navigate(MainRoute.RECORD), effect.await()) - } + assertEquals(1, viewModel.state.value.page) + assertEquals(OnboardingEffect.Navigate(MainRoute.RECORD), effect.await()) + } @Test - fun `Given onboarding has no storage When completed Then storage error code is exposed`() = runTest { - val viewModel = OnboardingViewModel( - preferencesRepository = FakeAppPreferencesRepository(), - documentStorage = FakeDocumentStorage(hasStorage = false), - ) + fun `Given onboarding has no recording archive When completed Then recording archive error code is exposed`() = + runTest { + val viewModel = OnboardingViewModel( + preferencesRepository = FakeAppPreferencesRepository(), + recordingArchive = FakeRecordingArchiveRepository(isSelected = false), + ) - viewModel.accept(OnboardingIntent.CompleteOnboarding) + viewModel.accept(OnboardingIntent.CompleteOnboarding) - assertEquals(AppErrorCode.STORAGE, viewModel.state.value.errorCode) - } + assertEquals(AppErrorCode.STORAGE, viewModel.state.value.errorCode) + } @Test - fun `Given settings action When sampling changes Then settings owns updated preference state`() = runTest { + fun `Given settings intent When sampling changes Then settings owns updated preference state`() = runTest { val viewModel = SettingsViewModel( preferencesRepository = FakeAppPreferencesRepository(), diagnosticsStore = FakeDiagnosticsStore(), @@ -86,7 +88,7 @@ class FeatureViewModelTest { } @Test - fun `Given settings action When theme changes Then settings owns updated appearance state`() = runTest { + fun `Given settings intent When theme changes Then settings owns updated appearance state`() = runTest { val viewModel = SettingsViewModel( preferencesRepository = FakeAppPreferencesRepository(), diagnosticsStore = FakeDiagnosticsStore(), @@ -124,32 +126,32 @@ class FeatureViewModelTest { val viewModel = recordingViewModel(FakeRecordingWorkflow()) advanceUntilIdle() - viewModel.accept(RecordingIntent.StartMeasurement) + viewModel.accept(RecordingIntent.StartRecording) assertEquals(RecordingMessage.PICK_AT_LEAST_ONE_SOURCE, viewModel.state.value.message) assertEquals(AppErrorCode.VALIDATION, viewModel.state.value.errorCode) } @Test - fun `Given Wear permission rejection When recording starts Then Wear guidance is shown`() = runTest { + fun `Given watch permission rejection When recording starts Then watch guidance is shown`() = runTest { val workflow = FakeRecordingWorkflow().apply { startResult = AppResult.failure( AppError( code = AppErrorCode.PERMISSION, operation = "Handle paired PREPARE acknowledgement", diagnosticMessage = "Paired PREPARE permission rejected", - context = mapOf("source" to "wear"), + context = mapOf("source" to "watch"), ), ) } val viewModel = recordingViewModel(workflow) advanceUntilIdle() - viewModel.accept(RecordingIntent.ToggleWearSensor(1)) - viewModel.accept(RecordingIntent.StartMeasurement) + viewModel.accept(RecordingIntent.ToggleWatchSensor(1)) + viewModel.accept(RecordingIntent.StartRecording) advanceUntilIdle() - assertEquals(RecordingMessage.WEAR_PERMISSION_REQUIRED, viewModel.state.value.message) + assertEquals(RecordingMessage.WATCH_PERMISSION_REQUIRED, viewModel.state.value.message) assertEquals(AppErrorCode.PERMISSION, viewModel.state.value.errorCode) } @@ -157,16 +159,16 @@ class FeatureViewModelTest { fun `Given waiting start When tapped Then progress appears and duplicate taps are ignored`() = runTest { val startGate = CompletableDeferred() val workflow = FakeRecordingWorkflow().apply { this.startGate = startGate } - val sessionStore = MeasurementSessionStore() + val sessionStore = RecordingSessionStore() val viewModel = recordingViewModel(workflow, sessionStore = sessionStore) advanceUntilIdle() viewModel.accept(RecordingIntent.ToggleSensor(1)) - viewModel.accept(RecordingIntent.StartMeasurement) + viewModel.accept(RecordingIntent.StartRecording) runCurrent() assertTrue(viewModel.state.value.isStarting) - viewModel.accept(RecordingIntent.StartMeasurement) + viewModel.accept(RecordingIntent.StartRecording) assertEquals(1, workflow.startCalls) startGate.complete(Unit) @@ -174,7 +176,7 @@ class FeatureViewModelTest { assertTrue(viewModel.state.value.isStarting) sessionStore.markRunning( - MeasurementSessionState.Running("session", "fixture", 0L, listOf(1), false), + RecordingSessionState.Running("session", "fixture", 0L, listOf(1), false), ) runCurrent() @@ -193,7 +195,7 @@ class FeatureViewModelTest { viewModel.accept(RecordingIntent.SetStartDelay(2)) // When - viewModel.accept(RecordingIntent.StartMeasurement) + viewModel.accept(RecordingIntent.StartRecording) runCurrent() // Then @@ -210,21 +212,21 @@ class FeatureViewModelTest { } @Test - fun `Given storage action When chosen Then recording emits only its picker effect`() = runTest { + fun `Given recording archive intent When chosen Then recording emits only its picker effect`() = runTest { val viewModel = recordingViewModel(FakeRecordingWorkflow()) val effect = async { viewModel.effects.first() } - viewModel.accept(RecordingIntent.ChooseStorage) + viewModel.accept(RecordingIntent.ChooseRecordingArchive) - assertEquals(RecordingEffect.PickStorageDirectory, effect.await()) + assertEquals(RecordingEffect.PickRecordingArchive, effect.await()) } @Test - fun `Given an active measurement When stopped Then recording navigates to the main screen`() = runTest { + fun `Given an active recording When stopped Then recording navigates to the main screen`() = runTest { val viewModel = recordingViewModel(FakeRecordingWorkflow()) val effect = async { viewModel.effects.first() } - viewModel.accept(RecordingIntent.StopMeasurement) + viewModel.accept(RecordingIntent.StopRecording) advanceUntilIdle() assertEquals(RecordingEffect.Navigate(MainRoute.RECORD), effect.await()) @@ -232,7 +234,7 @@ class FeatureViewModelTest { @Test fun `Given a Wear catalog When observed Then every sensor detail is retained`() = runTest { - val catalog = WearSensorCatalogStore() + val catalog = WatchSensorCatalogStore() val viewModel = recordingViewModel(FakeRecordingWorkflow(), catalog) advanceUntilIdle() @@ -271,37 +273,37 @@ class FeatureViewModelTest { reportingMode = com.tomasrepcik.sensorbox.domain.sensors.SensorReportingMode.CONTINUOUS, isWakeUpSensor = true, ), - viewModel.state.value.wearSensors.single(), + viewModel.state.value.watchSensors.single(), ) } private fun recordingViewModel( workflow: RecordingControlUseCase, - wearSensorCatalog: WearSensorCatalogStore = WearSensorCatalogStore(), - sessionStore: MeasurementSessionStore = MeasurementSessionStore(), + watchSensorCatalog: WatchSensorCatalogStore = WatchSensorCatalogStore(), + sessionStore: RecordingSessionStore = RecordingSessionStore(), ): RecordingViewModel { val repository = FakeWearConnectionRepository() return RecordingViewModel( preferencesRepository = FakeAppPreferencesRepository(), availableSensors = AvailableSensorsUseCase { emptyList() }, - storage = FakeDocumentStorage(hasStorage = true), - permissions = MeasurementPermissionsUseCase { emptySet() }, + recordingArchive = FakeRecordingArchiveRepository(isSelected = true), + permissions = RecordingPermissionsUseCase { emptySet() }, recording = workflow, sessionStore = sessionStore, - observeWearCapability = ObserveWearCapabilityUseCase(repository), - sendWearCommand = SendWearCommandUseCase(SendWearMessageUseCase(repository)), - wearSensorCatalog = wearSensorCatalog, + observeWatchCapability = ObserveWearCapabilityUseCase(repository), + sendWatchCommand = SendWearCommandUseCase(SendWearMessageUseCase(repository)), + watchSensorCatalog = watchSensorCatalog, elapsedRealtimeClock = ElapsedRealtimeClock { 10_000L }, ) } } -private class FakeDocumentStorage(private val hasStorage: Boolean) : DocumentStorageGateway { - override fun hasStorage(): AppResult = AppResult.success(hasStorage) +private class FakeRecordingArchiveRepository(private val isSelected: Boolean) : RecordingArchiveRepository { + override fun isSelected(): AppResult = AppResult.success(isSelected) - override fun displayPath(): AppResult = AppResult.success(if (hasStorage) "fixture" else null) + override fun path(): AppResult = AppResult.success(if (isSelected) "fixture" else null) - override fun persist(resultIntent: Intent): AppResult = AppResult.success(Unit) + override fun select(resultIntent: Intent): AppResult = AppResult.success(Unit) } private class FakeDiagnosticsStore( @@ -321,7 +323,7 @@ private class FakeRecordingWorkflow : RecordingControlUseCase { var startGate: CompletableDeferred? = null var startCalls = 0 - override suspend fun start(request: MeasurementRequest): AppResult { + override suspend fun start(request: RecordingSetup): AppResult { startCalls += 1 startGate?.await() return startResult diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingReducerTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingReducerTest.kt index f303da6..61bb469 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingReducerTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingReducerTest.kt @@ -15,12 +15,12 @@ class RecordingReducerTest { } @Test - fun `Given record state When storage is requested Then picker effect is emitted`() { + fun `Given record state When recording archive is requested Then picker effect is emitted`() { val givenState = RecordingStateFixtures.state() - val actual = RecordingReducer.reduce(givenState, RecordingIntent.ChooseStorage) + val actual = RecordingReducer.reduce(givenState, RecordingIntent.ChooseRecordingArchive) - assertEquals(RecordingEffect.PickStorageDirectory, actual.effect) + assertEquals(RecordingEffect.PickRecordingArchive, actual.effect) } @Test @@ -36,9 +36,9 @@ class RecordingReducerTest { fun `Given an unselected Wear sensor When toggled Then only Wear selection changes`() { val givenState = RecordingStateFixtures.state() - val actual = RecordingReducer.reduce(givenState, RecordingIntent.ToggleWearSensor(sensorId = 21)) + val actual = RecordingReducer.reduce(givenState, RecordingIntent.ToggleWatchSensor(sensorId = 21)) - assertTrue(21 in actual.state.selectedWearSensorIds) + assertTrue(21 in actual.state.selectedWatchSensorIds) assertTrue(actual.state.selectedSensorIds.isEmpty()) } @@ -64,9 +64,9 @@ class RecordingReducerTest { fun `Given selected sensors When setup is opened Then setup route is shown`() { val givenState = RecordingStateFixtures.state(selectedSensorIds = setOf(1)) - val actual = RecordingReducer.reduce(givenState, RecordingIntent.OpenMeasurementSetup) + val actual = RecordingReducer.reduce(givenState, RecordingIntent.OpenRecordingSetup) - assertEquals(RecordingEffect.Navigate(MainRoute.SETUP), actual.effect) + assertEquals(RecordingEffect.Navigate(MainRoute.RECORDING_SETUP), actual.effect) } @Test @@ -85,16 +85,16 @@ class RecordingReducerTest { val actual = RecordingReducer.reduce( givenState, - RecordingIntent.OpenSensorDetails(21, RecordingSensorSource.WEAR), + RecordingIntent.OpenSensorDetails(21, RecordingDevice.WATCH), ) assertEquals(RecordingEffect.Navigate(MainRoute.SENSOR_DETAILS), actual.effect) assertEquals(21, actual.state.detailsSensorType) - assertEquals(RecordingSensorSource.WEAR, actual.state.detailsSensorSource) + assertEquals(RecordingDevice.WATCH, actual.state.detailsDevice) } @Test - fun `Given measurement setup When returning Then sensor selection is shown`() { + fun `Given recording setup When returning Then sensor selection is shown`() { val givenState = RecordingStateFixtures.state(selectedSensorIds = setOf(1)) val actual = RecordingReducer.reduce(givenState, RecordingIntent.ReturnToSensorSelection) diff --git a/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingStateConversionTest.kt b/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingStateConversionTest.kt index 00d7c79..3c537c8 100644 --- a/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingStateConversionTest.kt +++ b/app/src/test/java/com/tomasrepcik/sensorbox/presentation/main/RecordingStateConversionTest.kt @@ -24,7 +24,7 @@ class RecordingStateConversionTest { ), ) - val request = state.toMeasurementRequest() + val request = state.toRecordingSetup() assertEquals(setOf(1), request.sensorIds) assertEquals(0, request.durationSeconds) diff --git a/build.gradle.kts b/build.gradle.kts index 590839f..0d3d52f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,7 +29,7 @@ data class ReadmeScreenshot(val module: String, val preview: String, val destina val readmeScreenshots = listOf( ReadmeScreenshot(":app", "readmeSourceSelection", "sensorbox-phone-record.png"), - ReadmeScreenshot(":app", "readmeMeasurementSetup", "sensorbox-phone-setup.png"), + ReadmeScreenshot(":app", "readmeRecordingSetup", "sensorbox-phone-setup.png"), ReadmeScreenshot(":app", "readmeIntroWelcome", "sensorbox-intro-welcome.png"), ReadmeScreenshot(":app", "readmeIntroPrivacy", "sensorbox-intro-privacy.png"), ReadmeScreenshot(":app", "readmeIntroPolicy", "sensorbox-intro-policy.png"), diff --git a/core-common/src/main/kotlin/com/tomasrepcik/sensorbox/core/error/AppError.kt b/core-common/src/main/kotlin/com/tomasrepcik/sensorbox/core/error/AppError.kt index 69634f8..eb42d6b 100644 --- a/core-common/src/main/kotlin/com/tomasrepcik/sensorbox/core/error/AppError.kt +++ b/core-common/src/main/kotlin/com/tomasrepcik/sensorbox/core/error/AppError.kt @@ -34,7 +34,7 @@ data class AppError( enum class AppErrorCode { CONNECTIVITY, EXTERNAL_ACTION, - MEASUREMENT, + RECORDING, PERMISSION, PREFERENCES, STORAGE, diff --git a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferences.kt b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferences.kt index a275f58..b2bb407 100644 --- a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferences.kt +++ b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferences.kt @@ -12,7 +12,7 @@ data class RecordingPreferences( val gpsIntervalSeconds: Int = 10, val gpsMinDistanceMeters: Int = 20, val sensorSamplingPeriod: Int = 0, - val restrictMeasurementOnLowBattery: Boolean = true, + val stopRecordingOnLowBattery: Boolean = true, val useWakeLock: Boolean = false, ) diff --git a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesIntent.kt b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesIntent.kt index bdac142..f5f928d 100644 --- a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesIntent.kt +++ b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesIntent.kt @@ -11,7 +11,7 @@ sealed interface AppPreferencesIntent { data class SetSensorSamplingPeriod(val period: Int) : AppPreferencesIntent - data class SetLowBatteryRestriction(val enabled: Boolean) : AppPreferencesIntent + data class SetStopOnLowBattery(val enabled: Boolean) : AppPreferencesIntent data class SetWakeLock(val enabled: Boolean) : AppPreferencesIntent diff --git a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesReducer.kt b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesReducer.kt index 0485e7c..d46b464 100644 --- a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesReducer.kt +++ b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/AppPreferencesReducer.kt @@ -34,8 +34,8 @@ object AppPreferencesReducer { display = current.display.copy(dynamicColors = intent.enabled), ) - is AppPreferencesIntent.SetLowBatteryRestriction -> current.copy( - recording = current.recording.copy(restrictMeasurementOnLowBattery = intent.enabled), + is AppPreferencesIntent.SetStopOnLowBattery -> current.copy( + recording = current.recording.copy(stopRecordingOnLowBattery = intent.enabled), ) is AppPreferencesIntent.SetSensorSamplingPeriod -> current.copy( diff --git a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/DataStoreAppPreferencesRepository.kt b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/DataStoreAppPreferencesRepository.kt index 43cf396..b0a2870 100644 --- a/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/DataStoreAppPreferencesRepository.kt +++ b/core/src/main/java/com/tomasrepcik/sensorbox/core/preferences/DataStoreAppPreferencesRepository.kt @@ -41,7 +41,7 @@ class DataStoreAppPreferencesRepository(private val dataStore: DataStore` with stable `AppErrorCode` values. Infrastructure adapters translate platform exceptions. Diagnostics remain local to the device. Tests use fakes at real seams and assert the outcomes seen by production callers. diff --git a/docs/recording-architecture-guided-review.md b/docs/recording-architecture-guided-review.md index 655dfd5..517e1f1 100644 --- a/docs/recording-architecture-guided-review.md +++ b/docs/recording-architecture-guided-review.md @@ -22,13 +22,13 @@ Read: - `../app/src/main/java/com/tomasrepcik/sensorbox/presentation/main/RecordScreen.kt` - `../app/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt` -The root UI sends actions to `RecordingViewModel`. The ViewModel owns the countdown in `startJob`. Until the countdown reaches zero, it does not call the recording use case. The UI only renders `RecordingState.startCountdownSeconds`. +The root UI sends intents to `RecordingViewModel`. The ViewModel owns the countdown in `startJob`. Until the countdown reaches zero, it does not call the recording use case. The UI only renders `RecordingState.startCountdownSeconds`. The useful seams injected into the ViewModel are small and named by intent: - `AvailableSensorsUseCase` -- `MeasurementPermissionsUseCase` -- `DocumentStorageGateway` +- `RecordingPermissionsUseCase` +- `RecordingArchiveRepository` - `RecordingControlUseCase` - `SendWearCommandUseCase` @@ -38,10 +38,10 @@ Check that these interfaces isolate real side effects or a module boundary. Ther Read: -- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/MeasurementControlUseCase.kt` +- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/RecordingControlUseCase.kt` - `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingCoordinator.kt` - `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PairedRecordingModel.kt` -- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/PhoneRecordingController.kt` +- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/recording/PhoneRecordingController.kt` The execution path is: @@ -50,14 +50,14 @@ RecordingViewModel -> RecordingControlUseCase -> PairedRecordingCoordinator -> PhoneRecordingController - -> SendWearCommandUseCase (only when Wear sources are selected) + -> SendWearCommandUseCase (only when watch sources are selected) ``` -`PairedRecordingCoordinator` owns one small state gate. It starts the phone first, sends a direct Wear start command, and cleans up both sides if Wear fails. It has no prepare/commit/rollback protocol and no mutex. Model-to-command conversions live beside the models in `PairedRecordingModel.kt`. +`PairedRecordingCoordinator` owns one small state gate. It starts the phone first, sends a direct watch start command, and cleans up both sides if the watch fails. It has no prepare/commit/rollback protocol and no mutex. Model-to-command conversions live beside the models in `PairedRecordingModel.kt`. -Check the stale-command rule: a stop received from Wear only affects the matching active session. +Check the stale-command rule: a stop received from the watch only affects the matching active recording session. -Also check the disconnected-timer rule. Once the Wear start command has been sent, losing its result does not tear down the phone recording. The phone and Wear services receive the same duration and each local `RecordingEngine` schedules its own stop. Automatic peer notifications are best effort after local cleanup. +Also check the disconnected-timer rule. Once the watch start command has been sent, losing its result does not tear down the phone recording. The phone and watch services receive the same duration and each local `RecordingEngine` schedules its own stop. Automatic peer notifications are best effort after local cleanup. ## 4. Review the JSON command boundary @@ -73,42 +73,42 @@ Commands are serializable data classes inside a versioned JSON envelope. Sensor Phone side: -- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearMessageDispatcher.kt` -- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWearCommandHandler.kt` -- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WearRecordingResultInbox.kt` +- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchMessageDispatcher.kt` +- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/PhoneWatchCommandHandler.kt` +- `../app/src/main/java/com/tomasrepcik/sensorbox/domain/paired/WatchRecordingResultInbox.kt` Wear side: - `../wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearMessageDispatcher.kt` - `../wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandHandler.kt` - `../wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandEnvironment.kt` -- `../wear/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementControlUseCase.kt` +- `../wear/src/main/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingControlUseCase.kt` - `../wear/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt` -Dispatchers decode and route. Handlers state-gate commands and execute them. On Wear, `WearRecordingRequirementsUseCase` isolates checks that are needed at the moment recording starts, while `WearRecordingControlUseCase` isolates starting and stopping the Android service. These are deliberate seams because they wrap separate effects and make command behavior testable. +Dispatchers decode and route. Handlers state-gate commands and execute them. On Wear, `WearRecordingRequirementsUseCase` isolates checks that are needed at the moment recording starts, while `WatchRecordingControlUseCase` isolates starting and stopping the Android service. These are deliberate seams because they wrap separate effects and make command behavior testable. ## 6. Follow the service into the recording engine Read: -- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementService.kt` -- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementHostResources.kt` +- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingService.kt` +- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingHostResources.kt` - `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceControllerFactory.kt` - `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceController.kt` - `../recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngine.kt` - `../recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingSource.kt` - `../recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingModel.kt` -`MeasurementService` routes lifecycle and intents. `MeasurementHostResources` owns Android host concerns such as notification, wake lock, receiver, and alarm. The factory assembles a controller. The controller translates the launch request into a direct recording plan. The engine then starts or stops ordered `RecordingSource` instances and knows nothing about Android. +`RecordingService` routes lifecycle and intents. `RecordingHostResources` owns Android host concerns such as notification, wake lock, receiver, and alarm. The factory assembles a controller. The controller translates the platform recording request into the engine recording request. The engine then starts or stops ordered `RecordingSource` instances and knows nothing about Android. -The key engine seam is `RecordingSource`. It is intentionally small: `start(spec)` and `stop()`. +The key engine seam is `RecordingSource`. It is intentionally small: `start(spec)` and `stop(context)`. ## 7. Review Android source construction Read: - `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/AndroidRecordingSources.kt` -- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionRecordingSource.kt` +- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionMetadataRecordingSource.kt` - `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SensorRecordingSource.kt` - `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/GpsRecordingSource.kt` - `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ActivityRecordingSource.kt` @@ -121,7 +121,7 @@ Read: Read: - `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/StorageHandler.kt` -- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ExtraInfoHandler.kt` +- `../sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/MeasurementMetadataWriter.kt` - `../core/src/main/java/com/tomasrepcik/sensorbox/core/error/FileDiagnostics.kt` - `../WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/files/WearFileMetadata.kt` - `../WearOsLib/src/main/java/com/tomasrepcik/sensorbox/wearoslib/files/WearFilePathCodec.kt` diff --git a/fastlane/metadata/android/en-GB/full_description.txt b/fastlane/metadata/android/en-GB/full_description.txt index 590a769..4a97309 100644 --- a/fastlane/metadata/android/en-GB/full_description.txt +++ b/fastlane/metadata/android/en-GB/full_description.txt @@ -1,19 +1,19 @@ -Use your sensors in mobile to track motion by accelerometer, gyroscope, magnetometer, ... and location data by GPS, which are stored in CSV files in your phone. No data are sent out from smartphone. +Use your phone sensors to record motion samples from the accelerometer, gyroscope, magnetometer, and other sensors, plus location samples from GPS. Samples are stored in CSV files on your phone. No data is sent away from your phone. -While measuring, you can make annotations, which you can customize in settings. They can be found in the same folder like CSV files called "extra". +While recording, you can add annotations and customize them in settings. They are stored with the measurement metadata. -App is constructed from more modules, which enable to set countdown to stop measurement, types of usage , use only motion sensors with/without GPS or just use all sensors. +Choose a recording duration and which recording sources to use, including motion sensors with or without GPS. -More parameters can be found in settings, with which you can modify measurement, like you want. +More choices for the recording setup are available in settings. Can use sensors: accelerometer, gyroscope, magnetometer, rotation vector calculation, thermometer, barometer, luxmeter, proximity sensor, humidity meter, pedometer. -If you are owner of Wear Os device, you can use it to get additional data. Wear motion data are collected by smartphone and stored in individual CSV files. Phone is required to collect data. Also, you can take a look at graphs directly in Wear Os with option to pause recording. +If you own a Wear OS device, you can use its sensors to record additional samples. Watch measurements can be synced to the recording archive on your phone. You can also view graphs directly on Wear OS. Recently, the activity recognition client , Google's API, has been added, so all motion changes, which can result in change of motion state, can be recorded. Two files are generated. One with timestamps of significant changes, which resulted in change of motion state. This data are event based. Second file has timestamps with confidence of each possible activity, which can client recognize. This data are recorded periodically with customizable frequency. Supported activities by activity recognition client: vehicle, bike, running, walking, still, tilting. -Wear Os app now supports local storage, so you can record data without paired phone. Sync can be done manually through phone app. Meanwhile local storage is in test phase, but available at all versions of app. +The Wear OS app supports local storage, so you can record without a paired phone. Sync can be started manually from the phone app. -More will come and if you have some kind of problem / suggestion, contact us via email down below. \ No newline at end of file +More will come and if you have some kind of problem / suggestion, contact us via email down below. diff --git a/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngine.kt b/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngine.kt index 54fe328..d600281 100644 --- a/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngine.kt +++ b/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngine.kt @@ -28,17 +28,17 @@ class RecordingEngine( val events: SharedFlow = mutableEvents.asSharedFlow() @Suppress("ReturnCount") - suspend fun start(plan: RecordingPlan): AppResult { - if (activeRecording != null) return conflict("Start recording", plan.sessionId) + suspend fun start(request: RecordingRequest): AppResult { + if (activeRecording != null) return conflict("Start recording", request.sessionId) - val validation = validate(plan) + val validation = validate(request) if (validation is AppResult.Failure) return validation - val recording = ActiveRecording(plan) + val recording = ActiveRecording(request) lastStopResult = null activeRecording = recording - for (spec in plan.sources.sortedBy { it.type.ordinal }) { + for (spec in request.sources.sortedBy { it.type.ordinal }) { val source = checkNotNull(sourceByType[spec.type]) recording.startedSources += source @@ -47,7 +47,7 @@ class RecordingEngine( if (activeRecording !== recording) return AppResult.success(Unit) } - mutableEvents.tryEmit(RecordingEvent.RecordingStarted(plan.sessionId)) + mutableEvents.tryEmit(RecordingEvent.RecordingStarted(request.sessionId)) scheduleDurationStop(recording) observeSourceFailures(recording) return AppResult.success(Unit) @@ -68,11 +68,11 @@ class RecordingEngine( context.failures.forEach { add(AppResult.failure(it)) } addAll(stoppedSources) } - val result = results.combineAppResults(AppErrorCode.MEASUREMENT, "Stop recording sources") + val result = results.combineAppResults(AppErrorCode.RECORDING, "Stop recording sources") lastStopResult = result mutableEvents.tryEmit( RecordingEvent.RecordingStopped( - sessionId = recording.plan.sessionId, + sessionId = recording.request.sessionId, reason = context.reason, result = result, ), @@ -88,7 +88,7 @@ class RecordingEngine( RecordingStopContext(RecordingStopReason.SOURCE_FAILURE, listOf(startError)), ) val result = (listOf(AppResult.failure(startError)) + stoppedSources) - .combineAppResults(AppErrorCode.MEASUREMENT, "Stop failed recording start") + .combineAppResults(AppErrorCode.RECORDING, "Stop failed recording start") return result } @@ -121,16 +121,16 @@ class RecordingEngine( } } - private fun validate(plan: RecordingPlan): AppResult { + private fun validate(request: RecordingRequest): AppResult { val invalidReason = when { - plan.sources.isEmpty() -> "Recording plan has no sources" + request.sources.isEmpty() -> "Recording request has no sources" - plan.durationMillis < 0L -> "Recording duration is negative" + request.durationMillis < 0L -> "Recording duration is negative" - plan.sources.map(RecordingSourceSpec::type).distinct().size != plan.sources.size -> - "Recording plan repeats a source type" + request.sources.map(RecordingSourceSpec::type).distinct().size != request.sources.size -> + "Recording request repeats a source type" - plan.sources.any { it.type !in sourceByType } -> "Recording source adapter is missing" + request.sources.any { it.type !in sourceByType } -> "Recording source adapter is missing" else -> null } @@ -138,15 +138,15 @@ class RecordingEngine( return if (invalidReason == null) { AppResult.success(Unit) } else { - AppResult.failure(AppError(AppErrorCode.VALIDATION, "Validate recording plan", invalidReason)) + AppResult.failure(AppError(AppErrorCode.VALIDATION, "Validate recording request", invalidReason)) } } private fun scheduleDurationStop(recording: ActiveRecording) { - val plan = recording.plan - if (plan.durationMillis <= 0L) return + val request = recording.request + if (request.durationMillis <= 0L) return recording.durationStop = scope.launch { - waitFor(plan.durationMillis) + waitFor(request.durationMillis) stop(RecordingStopReason.DURATION_EXPIRED) } } @@ -162,7 +162,7 @@ class RecordingEngine( ) private class ActiveRecording( - val plan: RecordingPlan, + val request: RecordingRequest, val startedSources: MutableList = mutableListOf(), var durationStop: Job? = null, var failureMonitor: Job? = null, diff --git a/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingModel.kt b/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingModel.kt index 2373e1f..9addf67 100644 --- a/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingModel.kt +++ b/recording-core/src/main/kotlin/com/tomasrepcik/sensorbox/recording/RecordingModel.kt @@ -11,7 +11,7 @@ value class RecordingSessionId(val value: String) { } enum class RecordingSourceType { - SESSION, + SESSION_METADATA, SENSOR, GPS, ACTIVITY_RECOGNITION, @@ -21,8 +21,8 @@ enum class RecordingSourceType { sealed interface RecordingSourceSpec { val type: RecordingSourceType - data object Session : RecordingSourceSpec { - override val type = RecordingSourceType.SESSION + data object SessionMetadata : RecordingSourceSpec { + override val type = RecordingSourceType.SESSION_METADATA } data class Sensors(val sensorTypes: Set, val samplingPeriod: Int) : RecordingSourceSpec { @@ -42,7 +42,7 @@ sealed interface RecordingSourceSpec { } } -data class RecordingPlan( +data class RecordingRequest( val sessionId: RecordingSessionId, val sources: List, val durationMillis: Long = 0L, diff --git a/recording-core/src/test/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngineTest.kt b/recording-core/src/test/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngineTest.kt index 96c158e..cf27d67 100644 --- a/recording-core/src/test/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngineTest.kt +++ b/recording-core/src/test/kotlin/com/tomasrepcik/sensorbox/recording/RecordingEngineTest.kt @@ -35,13 +35,13 @@ class RecordingEngineTest { FakeSource(RecordingSourceType.GPS, calls), FakeSource(RecordingSourceType.SENSOR, calls), ) - val plan = plan( + val request = request( RecordingSourceSpec.Gps(10, 20), RecordingSourceSpec.Sensors(setOf(1), 0), ) // When - val result = engine.start(plan) + val result = engine.start(request) // Then assertTrue(result.isSuccess) @@ -56,13 +56,13 @@ class RecordingEngineTest { FakeSource(RecordingSourceType.GPS, calls, startFails = true), FakeSource(RecordingSourceType.SENSOR, calls), ) - val plan = plan( + val request = request( RecordingSourceSpec.Sensors(setOf(1), 0), RecordingSourceSpec.Gps(10, 20), ) // When - val result = engine.start(plan) + val result = engine.start(request) // Then assertTrue(result.isFailure) @@ -80,11 +80,11 @@ class RecordingEngineTest { FakeSource(RecordingSourceType.GPS, calls, stopFails = true), FakeSource(RecordingSourceType.SENSOR, calls, stopFails = true), ) - val plan = plan( + val request = request( RecordingSourceSpec.Sensors(setOf(1), 0), RecordingSourceSpec.Gps(10, 20), ) - engine.start(plan) + engine.start(request) // When val result = engine.stop(RecordingStopReason.USER_REQUEST) @@ -99,14 +99,14 @@ class RecordingEngineTest { fun `Given runtime and stop failures When session stops Then session receives every failure`() = runTest { // Given val calls = mutableListOf() - val session = FakeSource(RecordingSourceType.SESSION, calls) + val session = FakeSource(RecordingSourceType.SESSION_METADATA, calls) val sensor = FakeSource(RecordingSourceType.SENSOR, calls, stopFails = true) val engine = RecordingEngine( sources = listOf(session, sensor), scope = backgroundScope, waitFor = { kotlinx.coroutines.delay(it) }, ) - engine.start(plan(RecordingSourceSpec.Session, RecordingSourceSpec.Sensors(setOf(1), 0))) + engine.start(request(RecordingSourceSpec.SessionMetadata, RecordingSourceSpec.Sensors(setOf(1), 0))) // When sensor.reportFailure() @@ -124,8 +124,8 @@ class RecordingEngineTest { // Given val calls = mutableListOf() val engine = engine(FakeSource(RecordingSourceType.SENSOR, calls)) - val plan = plan(RecordingSourceSpec.Sensors(setOf(1), 0)) - engine.start(plan) + val request = request(RecordingSourceSpec.Sensors(setOf(1), 0)) + engine.start(request) // When val first = engine.stop(RecordingStopReason.USER_REQUEST) @@ -145,10 +145,10 @@ class RecordingEngineTest { scope = backgroundScope, waitFor = { kotlinx.coroutines.delay(it) }, ) - val plan = plan(RecordingSourceSpec.Sensors(setOf(1), 0), durationMillis = 500L) + val request = request(RecordingSourceSpec.Sensors(setOf(1), 0), durationMillis = 500L) // When - engine.start(plan) + engine.start(request) advanceTimeBy(500L) runCurrent() @@ -166,8 +166,8 @@ class RecordingEngineTest { scope = backgroundScope, waitFor = { kotlinx.coroutines.delay(it) }, ) - val plan = plan(RecordingSourceSpec.Sensors(setOf(1), 0)) - engine.start(plan) + val request = request(RecordingSourceSpec.Sensors(setOf(1), 0)) + engine.start(request) // When source.reportFailure() @@ -184,7 +184,7 @@ class RecordingEngineTest { waitFor = { }, ) - private fun plan(vararg sources: RecordingSourceSpec, durationMillis: Long = 0L) = RecordingPlan( + private fun request(vararg sources: RecordingSourceSpec, durationMillis: Long = 0L) = RecordingRequest( sessionId = RecordingSessionId("session"), sources = sources.toList(), durationMillis = durationMillis, @@ -214,10 +214,10 @@ private class FakeSource( } fun reportFailure() { - mutableFailures.tryEmit(AppError(AppErrorCode.MEASUREMENT, "Record $type")) + mutableFailures.tryEmit(AppError(AppErrorCode.RECORDING, "Record $type")) } private fun failure(operation: String): AppResult = AppResult.failure( - AppError(AppErrorCode.MEASUREMENT, operation), + AppError(AppErrorCode.RECORDING, operation), ) } diff --git a/sensorservices/src/main/AndroidManifest.xml b/sensorservices/src/main/AndroidManifest.xml index 87d7afd..c291b4e 100644 --- a/sensorservices/src/main/AndroidManifest.xml +++ b/sensorservices/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ tools:ignore="HighSamplingRate" /> = appResult(AppErrorCode.MEASUREMENT, "Stop GPS updates") { + fun gpsOff(): AppResult = appResult(AppErrorCode.RECORDING, "Stop GPS updates") { if (callback != null) { locationClient?.flushLocations()?.addOnFailureListener { error -> Log.e(TAG, "Cannot flush GPS updates", error) @@ -73,7 +73,7 @@ class GPSHandler(private val locationClient: FusedLocationProviderClient? = null intervalSeconds: Int = DEFAULT_INTERVAL_SECONDS, minDistanceMeters: Int = DEFAULT_DISTANCE_METERS, ): AppResult = (if (callback != null) gpsOff() else AppResult.success(Unit)).flatMap { - appResult(AppErrorCode.MEASUREMENT, "Register GPS callback") { + appResult(AppErrorCode.RECORDING, "Register GPS callback") { val client = checkNotNull(locationClient) { "GPS client is unavailable" } callback = gpsCallback initialize(client, intervalSeconds, minDistanceMeters) diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionPlatform.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionPlatform.kt index a569e50..e18086d 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionPlatform.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionPlatform.kt @@ -89,7 +89,7 @@ internal class AndroidActivityRecognitionPlatform(private val context: Context) } val prepared = prepareResources(onUpdate, onTransitions) if (prepared.isFailure) return prepared - return suspendAppResult(AppErrorCode.MEASUREMENT, "Start activity recognition") { + return suspendAppResult(AppErrorCode.RECORDING, "Start activity recognition") { val activeClient = checkNotNull(client) { "Activity recognition client is missing" } val updates = checkNotNull(updatesPendingIntent) { "Activity update request is missing" } val transitions = checkNotNull(transitionsPendingIntent) { "Activity transition request is missing" } @@ -104,7 +104,7 @@ internal class AndroidActivityRecognitionPlatform(private val context: Context) private fun prepareResources( onUpdate: (ActivityUpdate) -> Unit, onTransitions: (List) -> Unit, - ): AppResult = appResult(AppErrorCode.MEASUREMENT, "Initialize activity recognition resources") { + ): AppResult = appResult(AppErrorCode.RECORDING, "Initialize activity recognition resources") { updateCallback = onUpdate transitionCallback = onTransitions val filter = IntentFilter(ACTION_UPDATE).apply { addAction(ACTION_TRANSITION) } @@ -129,16 +129,16 @@ internal class AndroidActivityRecognitionPlatform(private val context: Context) override suspend fun stop(): AppResult { val results = mutableListOf>() updatesPendingIntent?.let { pendingIntent -> - results += suspendAppResult(AppErrorCode.MEASUREMENT, "Remove activity updates") { + results += suspendAppResult(AppErrorCode.RECORDING, "Remove activity updates") { client?.removeActivityUpdates(pendingIntent)?.await() } } transitionsPendingIntent?.let { pendingIntent -> - results += suspendAppResult(AppErrorCode.MEASUREMENT, "Remove activity transitions") { + results += suspendAppResult(AppErrorCode.RECORDING, "Remove activity transitions") { client?.removeActivityTransitionUpdates(pendingIntent)?.await() } } - results += appResult(AppErrorCode.MEASUREMENT, "Release activity recognition resources") { + results += appResult(AppErrorCode.RECORDING, "Release activity recognition resources") { if (receiverRegistered) context.unregisterReceiver(receiver) receiverRegistered = false client = null @@ -147,7 +147,7 @@ internal class AndroidActivityRecognitionPlatform(private val context: Context) updateCallback = null transitionCallback = null } - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Pause activity recognition") + return results.combineAppResults(AppErrorCode.RECORDING, "Pause activity recognition") } private fun hasPermission(): Boolean = Build.VERSION.SDK_INT < 29 || diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionMeasurement.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionRecording.kt similarity index 87% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionMeasurement.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionRecording.kt index 667f08f..5aea36c 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionMeasurement.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionRecording.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.asSharedFlow import java.io.OutputStream /** Records periodic activity-recognition confidence values. */ -internal class ActivityRecognitionMeasurement( +internal class ActivityRecognitionRecording( private val storage: MeasurementStorage, private val platform: ActivityRecognitionPlatform, ) { @@ -28,7 +28,7 @@ internal class ActivityRecognitionMeasurement( val opened = openOutputs(folderName, useInternalStorage) if (opened.isFailure) return opened return platform.start(periodSeconds, ::writeActivityUpdate, ::writeActivityTransitions) - .withAppError(AppErrorCode.MEASUREMENT, "Start activity recognition") + .withAppError(AppErrorCode.RECORDING, "Start activity recognition") } private fun openOutputs(folderName: String, useInternalStorage: Boolean): AppResult { @@ -40,7 +40,7 @@ internal class ActivityRecognitionMeasurement( ) val updates = updatesResult.getOrNull() ?: return AppResult.failure(checkNotNull(updatesResult.errorOrNull())) - .withAppError(AppErrorCode.MEASUREMENT, "Initialize activity recognition") + .withAppError(AppErrorCode.RECORDING, "Initialize activity recognition") updatesOutput = updates val transitionsResult = storage.openMeasurementFile( @@ -51,10 +51,10 @@ internal class ActivityRecognitionMeasurement( ) val transitions = transitionsResult.getOrNull() if (transitions == null) { - appResult(AppErrorCode.STORAGE, "Close incomplete activity measurement") { updates.close() } + appResult(AppErrorCode.STORAGE, "Close incomplete activity measurement file") { updates.close() } updatesOutput = null return AppResult.failure(checkNotNull(transitionsResult.errorOrNull())) - .withAppError(AppErrorCode.MEASUREMENT, "Initialize activity recognition") + .withAppError(AppErrorCode.RECORDING, "Initialize activity recognition") } transitionsOutput = transitions @@ -62,12 +62,12 @@ internal class ActivityRecognitionMeasurement( updates.write(UPDATES_HEADER.toByteArray()) transitions.write(TRANSITIONS_HEADER.toByteArray()) } - return headers.withAppError(AppErrorCode.MEASUREMENT, "Initialize activity recognition") + return headers.withAppError(AppErrorCode.RECORDING, "Initialize activity recognition") } suspend fun stop(): AppResult { val results = listOf(platform.stop(), save()) - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Stop activity recognition") + return results.combineAppResults(AppErrorCode.RECORDING, "Stop activity recognition") } private fun writeActivityUpdate(update: ActivityUpdate) { @@ -96,7 +96,7 @@ internal class ActivityRecognitionMeasurement( updatesOutput = null transitionsOutput = null writeFailure = null - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Save activity recognition") + return results.combineAppResults(AppErrorCode.RECORDING, "Save activity recognition") } private inline fun recordWriteFailure(operation: String, block: () -> Unit) { diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/GPSMeasurement.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/GpsRecording.kt similarity index 86% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/GPSMeasurement.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/GpsRecording.kt index dcb36f6..83d783e 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/GPSMeasurement.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/GpsRecording.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.withContext import java.io.OutputStream -internal class GPSMeasurement( +internal class GpsRecording( private val gpsHandler: GPSHandler, private val storage: MeasurementStorage, private val clock: EpochClock, @@ -37,7 +37,7 @@ internal class GPSMeasurement( minimumDistanceMeters: Int, ): AppResult = openOutput(folderName, useInternalStorage).flatMap { gpsHandler.addCallback(this, intervalSeconds, minimumDistanceMeters) - .withAppError(AppErrorCode.MEASUREMENT, "Start GPS") + .withAppError(AppErrorCode.RECORDING, "Start GPS") } private fun openOutput(folderName: String, useInternalStorage: Boolean): AppResult { @@ -49,12 +49,12 @@ internal class GPSMeasurement( ) val output = streamResult.getOrNull() ?: return AppResult.failure(checkNotNull(streamResult.errorOrNull())) - .withAppError(AppErrorCode.MEASUREMENT, "Initialize GPS measurement") + .withAppError(AppErrorCode.RECORDING, "Initialize GPS recording") outputStream = output val headerResult = appResult(AppErrorCode.STORAGE, "Write GPS header") { output.write(HEADER.toByteArray()) } - return headerResult.withAppError(AppErrorCode.MEASUREMENT, "Initialize GPS measurement") + return headerResult.withAppError(AppErrorCode.RECORDING, "Initialize GPS recording") } private fun locationRow(location: Location): String = "${clock.nowMillis()};" + @@ -67,23 +67,23 @@ internal class GPSMeasurement( location.provider + "\n" private fun pause(): AppResult = gpsHandler.gpsOff() - .withAppError(AppErrorCode.MEASUREMENT, "Pause GPS") + .withAppError(AppErrorCode.RECORDING, "Pause GPS") private fun save(): AppResult { val stream = outputStream val results = mutableListOf>() - results += appResult(AppErrorCode.STORAGE, "Flush GPS measurement") { stream?.flush() } + results += appResult(AppErrorCode.STORAGE, "Flush GPS measurement file") { stream?.flush() } writeFailure?.let { results += AppResult.failure(it) } - results += appResult(AppErrorCode.STORAGE, "Close GPS measurement") { stream?.close() } + results += appResult(AppErrorCode.STORAGE, "Close GPS measurement file") { stream?.close() } outputStream = null writeFailure = null - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Save GPS measurement") + return results.combineAppResults(AppErrorCode.RECORDING, "Stop GPS recording") } suspend fun stop(): AppResult = listOf( withContext(Dispatchers.Main) { pause() }, withContext(Dispatchers.IO) { save() }, - ).combineAppResults(AppErrorCode.MEASUREMENT, "Stop GPS measurement") + ).combineAppResults(AppErrorCode.RECORDING, "Stop GPS recording") override fun onLocationChanged(location: Location?) { val sample = location ?: return diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ExtraInfoHandler.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/MeasurementMetadataWriter.kt similarity index 93% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ExtraInfoHandler.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/MeasurementMetadataWriter.kt index 6225cfd..15084e9 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ExtraInfoHandler.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/MeasurementMetadataWriter.kt @@ -13,26 +13,26 @@ import com.tomasrepcik.sensorbox.core.time.ClockFormats import com.tomasrepcik.sensorbox.core.time.EpochClock import com.tomasrepcik.sensorbox.recording.RecordingStopContext import com.tomasrepcik.sensorbox.sensorservices.handlers.MeasurementStorage -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest import com.tomasrepcik.sensorbox.sensorservices.types.SensorFileStats import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json /** Collects session metadata and writes it once when the recording is closed. */ -internal class ExtraInfoHandler( +internal class MeasurementMetadataWriter( private val storage: MeasurementStorage, private val clock: EpochClock, private val sensorManager: SensorManager, ) { - private var request: MeasurementLaunchRequest? = null + private var request: RecordingRequest? = null private var startedAtMillis: Long = 0L private var startedAtNanos: Long = 0L private var sensorStats: List = emptyList() private val annotations = mutableListOf() private val triggeredAlarms = mutableListOf() - fun start(request: MeasurementLaunchRequest) { + fun start(request: RecordingRequest) { this.request = request val startedAt = capturePhoneTime() startedAtMillis = startedAt.unixMillis @@ -58,7 +58,7 @@ internal class ExtraInfoHandler( val active = request ?: return AppResult.success(Unit) request = null val endedAt = capturePhoneTime() - val metadataResult = appResult(AppErrorCode.MEASUREMENT, "Build measurement metadata") { + val metadataResult = appResult(AppErrorCode.RECORDING, "Build measurement metadata") { MeasurementMetadata( millis = startedAtMillis, nanos = startedAtNanos, @@ -96,10 +96,10 @@ internal class ExtraInfoHandler( output.use { it.write(JSON.encodeToString(metadata).toByteArray()) } } } - }.withAppError(AppErrorCode.MEASUREMENT, "Write measurement metadata") + }.withAppError(AppErrorCode.RECORDING, "Write measurement metadata") } - private fun sensorRanges(active: MeasurementLaunchRequest): List { + private fun sensorRanges(active: RecordingRequest): List { val rangeIds = if (active.significantMotion) { active.sensorIds + Sensor.TYPE_SIGNIFICANT_MOTION } else { diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SensorMeasurement.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SensorRecording.kt similarity index 83% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SensorMeasurement.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SensorRecording.kt index e75015f..6294e1b 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SensorMeasurement.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SensorRecording.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -internal class SensorMeasurement( +internal class SensorRecording( private val storage: MeasurementStorage, private val diagnosticLogger: DiagnosticLogger, private val sensorManager: SensorManager? = null, @@ -42,7 +42,7 @@ internal class SensorMeasurement( val holderResult = createHolder(folderName, useInternalStorage, sensorType) if (holderResult.isFailure) { return AppResult.failure(checkNotNull(holderResult.errorOrNull())) - .withAppError(AppErrorCode.MEASUREMENT, "Initialize sensors") + .withAppError(AppErrorCode.RECORDING, "Initialize sensors") } holderResult.getOrNull()?.let(holders::add) } @@ -67,11 +67,11 @@ internal class SensorMeasurement( private fun registerSensors(samplingPeriod: Int): AppResult { val sensorManager = sensorManager - ?: return AppResult.failure(AppError(AppErrorCode.MEASUREMENT, "Access sensor manager")) + ?: return AppResult.failure(AppError(AppErrorCode.RECORDING, "Access sensor manager")) for (holder in holders) { val registration = registerHolder(sensorManager, holder, samplingPeriod) if (registration.isFailure) { - return registration.withAppError(AppErrorCode.MEASUREMENT, "Start sensors") + return registration.withAppError(AppErrorCode.RECORDING, "Start sensors") } } return AppResult.success(Unit) @@ -83,8 +83,8 @@ internal class SensorMeasurement( samplingPeriod: Int, ): AppResult { val sensor = sensorManager.getDefaultSensor(holder.spec.type) - ?: return AppResult.failure(AppError(AppErrorCode.MEASUREMENT, "Find sensor ${holder.spec.type}")) - val registration = appResult(AppErrorCode.MEASUREMENT, "Register sensor ${holder.spec.type}") { + ?: return AppResult.failure(AppError(AppErrorCode.RECORDING, "Find sensor ${holder.spec.type}")) + val registration = appResult(AppErrorCode.RECORDING, "Register sensor ${holder.spec.type}") { sensorManager.registerListener(holder, sensor, samplingPeriod) } return if (registration.getOrNull() == true) { @@ -92,12 +92,12 @@ internal class SensorMeasurement( } else { AppResult.failure( registration.errorOrNull() - ?: AppError(AppErrorCode.MEASUREMENT, "Register sensor ${holder.spec.type}"), + ?: AppError(AppErrorCode.RECORDING, "Register sensor ${holder.spec.type}"), ) } } - private fun pause(): AppResult = appResult(AppErrorCode.MEASUREMENT, "Pause sensors") { + private fun pause(): AppResult = appResult(AppErrorCode.RECORDING, "Pause sensors") { sensorManager?.let { manager -> holders.forEach(manager::unregisterListener) } } @@ -105,11 +105,11 @@ internal class SensorMeasurement( val results = holders.map { it.close() } onStopped(holders.map(SensorHolder::stats)) holders.clear() - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Save sensors") + return results.combineAppResults(AppErrorCode.RECORDING, "Save sensors") } suspend fun stop(): AppResult = listOf( pause(), save(), - ).combineAppResults(AppErrorCode.MEASUREMENT, "Stop sensors") + ).combineAppResults(AppErrorCode.RECORDING, "Stop sensors") } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SignificantMotion.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SignificantMotionRecording.kt similarity index 83% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SignificantMotion.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SignificantMotionRecording.kt index 320e3b5..307c558 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SignificantMotion.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/SignificantMotionRecording.kt @@ -18,8 +18,10 @@ import kotlinx.coroutines.flow.asSharedFlow import java.io.OutputStream /** Handles Android's one-shot significant-motion trigger and re-arms it after every event. */ -internal class SignificantMotion(private val storage: MeasurementStorage, private val sensorManager: SensorManager) : - TriggerEventListener() { +internal class SignificantMotionRecording( + private val storage: MeasurementStorage, + private val sensorManager: SensorManager, +) : TriggerEventListener() { private var sensor: Sensor? = null private var output: OutputStream? = null private var writeFailure: AppError? = null @@ -32,7 +34,7 @@ internal class SignificantMotion(private val storage: MeasurementStorage, privat if (arm()) { AppResult.success(Unit) } else { - AppResult.failure(AppError(AppErrorCode.MEASUREMENT, "Start significant motion")) + AppResult.failure(AppError(AppErrorCode.RECORDING, "Start significant motion")) } } @@ -49,11 +51,11 @@ internal class SignificantMotion(private val storage: MeasurementStorage, privat appResult(AppErrorCode.STORAGE, "Write significant motion header") { opened.write("t_sensor;event\n".toByteArray()) } - }.withAppError(AppErrorCode.MEASUREMENT, "Initialize significant motion") + }.withAppError(AppErrorCode.RECORDING, "Initialize significant motion") } private fun pause(): AppResult = appResult( - AppErrorCode.MEASUREMENT, + AppErrorCode.RECORDING, "Pause significant motion", ) { sensor?.let { sensorManager.cancelTriggerSensor(this, it) } @@ -65,13 +67,13 @@ internal class SignificantMotion(private val storage: MeasurementStorage, privat writeFailure?.let { results += AppResult.failure(it) } output = null writeFailure = null - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Save significant motion") + return results.combineAppResults(AppErrorCode.RECORDING, "Save significant motion") } suspend fun stop(): AppResult { val results = listOf(pause(), save()) sensor = null - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Stop significant motion") + return results.combineAppResults(AppErrorCode.RECORDING, "Stop significant motion") } override fun onTrigger(event: TriggerEvent?) { @@ -80,7 +82,7 @@ internal class SignificantMotion(private val storage: MeasurementStorage, privat output?.write("${event.timestamp};$value\n".toByteArray()) }.onFailure(::reportFailure) } - if (!arm()) reportFailure(AppError(AppErrorCode.MEASUREMENT, "Re-arm significant motion")) + if (!arm()) reportFailure(AppError(AppErrorCode.RECORDING, "Re-arm significant motion")) } private fun reportFailure(error: AppError) { diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementIntentFactory.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementIntentFactory.kt deleted file mode 100644 index 924e37d..0000000 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementIntentFactory.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tomasrepcik.sensorbox.sensorservices.intent - -import android.content.Context -import android.content.Intent -import com.tomasrepcik.sensorbox.core.time.ClockFormats -import com.tomasrepcik.sensorbox.core.time.EpochClock -import com.tomasrepcik.sensorbox.sensorservices.services.MeasurementService -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -class MeasurementIntentFactory @Inject constructor( - @ApplicationContext private val context: Context, - private val clock: EpochClock, -) { - fun create(request: MeasurementLaunchRequest): Intent = Intent(context, MeasurementService::class.java).apply { - putExtra(MeasurementService.SESSION_ID, request.sessionId) - putExtra(MeasurementService.FOLDER_NAME, request.folderName) - putExtra(MeasurementService.INTERNAL_STORAGE, request.useInternalStorage) - putExtra(MeasurementService.ANDROID_SENSORS, request.sensorIds.toIntArray()) - putExtra(MeasurementService.ANDROID_SENSORS_SPEED, request.sensorSamplingPeriod) - putExtra(MeasurementService.GPS, request.includesGps) - putExtra(MeasurementService.STOP_ON_LOW_BATTERY, request.stopOnLowBattery) - putExtra(MeasurementService.USE_WAKE_LOCK, request.useWakeLock) - putExtra(MeasurementService.GPS_INTERVAL_SECONDS, request.gpsIntervalSeconds) - putExtra(MeasurementService.GPS_DISTANCE_METERS, request.gpsMinDistanceMeters) - putExtra(MeasurementService.DURATION_MILLIS, request.durationMillis) - putStringArrayListExtra(MeasurementService.NOTES, ArrayList(request.notes)) - putExtra(MeasurementService.ALARM_OFFSETS_SECONDS, request.alarmOffsetsSeconds.toIntArray()) - putExtra(MeasurementService.ACTIVITY_RECOGNITION, request.activityRecognition) - putExtra(MeasurementService.ACTIVITY_RECOGNITION_PERIOD_SECONDS, request.activityRecognitionPeriodSeconds) - putExtra(MeasurementService.SIGNIFICANT_MOTION, request.significantMotion) - } - - fun newFolderName(customName: String = ""): String { - val prefix = customName.trim().replace(INVALID_NAME_CHARS, "_").trim('_').take(MAX_PREFIX_LENGTH) - .ifBlank { "recording" } - val timestamp = ClockFormats.folderTimestamp(clock.nowMillis()) - return "${prefix}_$timestamp" - } - - private companion object { - const val MAX_PREFIX_LENGTH = 60 - val INVALID_NAME_CHARS = Regex("[^A-Za-z0-9._-]+") - } -} diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementLaunchRequest.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementLaunchRequest.kt deleted file mode 100644 index 6089514..0000000 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/MeasurementLaunchRequest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tomasrepcik.sensorbox.sensorservices.intent - -import android.content.Intent -import android.hardware.SensorManager -import com.tomasrepcik.sensorbox.sensorservices.services.MeasurementService -import java.util.UUID - -data class MeasurementLaunchRequest( - val sessionId: String = UUID.randomUUID().toString(), - val folderName: String, - val useInternalStorage: Boolean, - val sensorIds: Set, - val sensorSamplingPeriod: Int, - val includesGps: Boolean, - val stopOnLowBattery: Boolean, - val useWakeLock: Boolean, - val gpsIntervalSeconds: Int, - val gpsMinDistanceMeters: Int, - val durationMillis: Long = 0L, - val notes: List = emptyList(), - val alarmOffsetsSeconds: List = emptyList(), - val activityRecognition: Boolean = false, - val activityRecognitionPeriodSeconds: Int = 30, - val significantMotion: Boolean = false, -) { - val requiresWakeLock: Boolean - get() = useWakeLock || sensorIds.isNotEmpty() - - companion object { - fun from(intent: Intent): MeasurementLaunchRequest = MeasurementLaunchRequest( - sessionId = intent.getStringExtra(MeasurementService.SESSION_ID).orEmpty(), - folderName = intent.getStringExtra(MeasurementService.FOLDER_NAME).orEmpty(), - useInternalStorage = intent.getBooleanExtra(MeasurementService.INTERNAL_STORAGE, false), - sensorIds = intent.getIntArrayExtra(MeasurementService.ANDROID_SENSORS)?.toSet().orEmpty(), - sensorSamplingPeriod = intent.getIntExtra( - MeasurementService.ANDROID_SENSORS_SPEED, - SensorManager.SENSOR_DELAY_FASTEST, - ), - includesGps = intent.getBooleanExtra(MeasurementService.GPS, false), - stopOnLowBattery = intent.getBooleanExtra(MeasurementService.STOP_ON_LOW_BATTERY, false), - useWakeLock = intent.getBooleanExtra(MeasurementService.USE_WAKE_LOCK, false), - gpsIntervalSeconds = intent.getIntExtra(MeasurementService.GPS_INTERVAL_SECONDS, 10), - gpsMinDistanceMeters = intent.getIntExtra(MeasurementService.GPS_DISTANCE_METERS, 20), - durationMillis = intent.getLongExtra(MeasurementService.DURATION_MILLIS, 0L).coerceAtLeast(0L), - notes = intent.getStringArrayListExtra(MeasurementService.NOTES).orEmpty(), - alarmOffsetsSeconds = intent.getIntArrayExtra(MeasurementService.ALARM_OFFSETS_SECONDS) - ?.filter { it >= 0 }.orEmpty(), - activityRecognition = intent.getBooleanExtra(MeasurementService.ACTIVITY_RECOGNITION, false), - activityRecognitionPeriodSeconds = intent.getIntExtra( - MeasurementService.ACTIVITY_RECOGNITION_PERIOD_SECONDS, - 30, - ).coerceAtLeast(1), - significantMotion = intent.getBooleanExtra(MeasurementService.SIGNIFICANT_MOTION, false), - ) - } -} diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingIntentFactory.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingIntentFactory.kt new file mode 100644 index 0000000..c20e461 --- /dev/null +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingIntentFactory.kt @@ -0,0 +1,45 @@ +package com.tomasrepcik.sensorbox.sensorservices.intent + +import android.content.Context +import android.content.Intent +import com.tomasrepcik.sensorbox.core.time.ClockFormats +import com.tomasrepcik.sensorbox.core.time.EpochClock +import com.tomasrepcik.sensorbox.sensorservices.services.RecordingService +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class RecordingIntentFactory @Inject constructor( + @ApplicationContext private val context: Context, + private val clock: EpochClock, +) { + fun create(request: RecordingRequest): Intent = Intent(context, RecordingService::class.java).apply { + putExtra(RecordingService.SESSION_ID, request.sessionId) + putExtra(RecordingService.FOLDER_NAME, request.folderName) + putExtra(RecordingService.INTERNAL_STORAGE, request.useInternalStorage) + putExtra(RecordingService.ANDROID_SENSORS, request.sensorIds.toIntArray()) + putExtra(RecordingService.ANDROID_SENSORS_SPEED, request.sensorSamplingPeriod) + putExtra(RecordingService.GPS, request.includesGps) + putExtra(RecordingService.STOP_ON_LOW_BATTERY, request.stopOnLowBattery) + putExtra(RecordingService.USE_WAKE_LOCK, request.useWakeLock) + putExtra(RecordingService.GPS_INTERVAL_SECONDS, request.gpsIntervalSeconds) + putExtra(RecordingService.GPS_DISTANCE_METERS, request.gpsMinDistanceMeters) + putExtra(RecordingService.DURATION_MILLIS, request.durationMillis) + putStringArrayListExtra(RecordingService.NOTES, ArrayList(request.notes)) + putExtra(RecordingService.ALARM_OFFSETS_SECONDS, request.alarmOffsetsSeconds.toIntArray()) + putExtra(RecordingService.ACTIVITY_RECOGNITION, request.activityRecognition) + putExtra(RecordingService.ACTIVITY_RECOGNITION_PERIOD_SECONDS, request.activityRecognitionPeriodSeconds) + putExtra(RecordingService.SIGNIFICANT_MOTION, request.significantMotion) + } + + fun newFolderName(customName: String = ""): String { + val prefix = customName.trim().replace(INVALID_NAME_CHARS, "_").trim('_').take(MAX_PREFIX_LENGTH) + .ifBlank { "recording" } + val timestamp = ClockFormats.folderTimestamp(clock.nowMillis()) + return "${prefix}_$timestamp" + } + + private companion object { + const val MAX_PREFIX_LENGTH = 60 + val INVALID_NAME_CHARS = Regex("[^A-Za-z0-9._-]+") + } +} diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingRequest.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingRequest.kt new file mode 100644 index 0000000..c8fb454 --- /dev/null +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/intent/RecordingRequest.kt @@ -0,0 +1,56 @@ +package com.tomasrepcik.sensorbox.sensorservices.intent + +import android.content.Intent +import android.hardware.SensorManager +import com.tomasrepcik.sensorbox.sensorservices.services.RecordingService +import java.util.UUID + +data class RecordingRequest( + val sessionId: String = UUID.randomUUID().toString(), + val folderName: String, + val useInternalStorage: Boolean, + val sensorIds: Set, + val sensorSamplingPeriod: Int, + val includesGps: Boolean, + val stopOnLowBattery: Boolean, + val useWakeLock: Boolean, + val gpsIntervalSeconds: Int, + val gpsMinDistanceMeters: Int, + val durationMillis: Long = 0L, + val notes: List = emptyList(), + val alarmOffsetsSeconds: List = emptyList(), + val activityRecognition: Boolean = false, + val activityRecognitionPeriodSeconds: Int = 30, + val significantMotion: Boolean = false, +) { + val requiresWakeLock: Boolean + get() = useWakeLock || sensorIds.isNotEmpty() + + companion object { + fun from(intent: Intent): RecordingRequest = RecordingRequest( + sessionId = intent.getStringExtra(RecordingService.SESSION_ID).orEmpty(), + folderName = intent.getStringExtra(RecordingService.FOLDER_NAME).orEmpty(), + useInternalStorage = intent.getBooleanExtra(RecordingService.INTERNAL_STORAGE, false), + sensorIds = intent.getIntArrayExtra(RecordingService.ANDROID_SENSORS)?.toSet().orEmpty(), + sensorSamplingPeriod = intent.getIntExtra( + RecordingService.ANDROID_SENSORS_SPEED, + SensorManager.SENSOR_DELAY_FASTEST, + ), + includesGps = intent.getBooleanExtra(RecordingService.GPS, false), + stopOnLowBattery = intent.getBooleanExtra(RecordingService.STOP_ON_LOW_BATTERY, false), + useWakeLock = intent.getBooleanExtra(RecordingService.USE_WAKE_LOCK, false), + gpsIntervalSeconds = intent.getIntExtra(RecordingService.GPS_INTERVAL_SECONDS, 10), + gpsMinDistanceMeters = intent.getIntExtra(RecordingService.GPS_DISTANCE_METERS, 20), + durationMillis = intent.getLongExtra(RecordingService.DURATION_MILLIS, 0L).coerceAtLeast(0L), + notes = intent.getStringArrayListExtra(RecordingService.NOTES).orEmpty(), + alarmOffsetsSeconds = intent.getIntArrayExtra(RecordingService.ALARM_OFFSETS_SECONDS) + ?.filter { it >= 0 }.orEmpty(), + activityRecognition = intent.getBooleanExtra(RecordingService.ACTIVITY_RECOGNITION, false), + activityRecognitionPeriodSeconds = intent.getIntExtra( + RecordingService.ACTIVITY_RECOGNITION_PERIOD_SECONDS, + 30, + ).coerceAtLeast(1), + significantMotion = intent.getBooleanExtra(RecordingService.SIGNIFICANT_MOTION, false), + ) + } +} diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ActivityRecordingSource.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ActivityRecordingSource.kt index 0c57781..2becec0 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ActivityRecordingSource.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ActivityRecordingSource.kt @@ -6,22 +6,22 @@ import com.tomasrepcik.sensorbox.recording.RecordingSourceSpec import com.tomasrepcik.sensorbox.recording.RecordingSourceType import com.tomasrepcik.sensorbox.recording.RecordingStopContext import com.tomasrepcik.sensorbox.sensorservices.handlers.MeasurementStorage -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.ActivityRecognitionMeasurement import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.ActivityRecognitionPlatform -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.ActivityRecognitionRecording +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest internal class ActivityRecordingSource( - private val request: MeasurementLaunchRequest, + private val request: RecordingRequest, private val storage: MeasurementStorage, private val platform: ActivityRecognitionPlatform, ) : RecordingSource { - private val measurement = ActivityRecognitionMeasurement(storage, platform) + private val recording = ActivityRecognitionRecording(storage, platform) override val type = RecordingSourceType.ACTIVITY_RECOGNITION - override val failures = measurement.failures + override val failures = recording.failures override suspend fun start(spec: RecordingSourceSpec): AppResult = if (spec is RecordingSourceSpec.ActivityRecognition) { - measurement.start( + recording.start( folderName = request.folderName, useInternalStorage = request.useInternalStorage, periodSeconds = spec.periodSeconds, @@ -30,5 +30,5 @@ internal class ActivityRecordingSource( invalidSpec(type) } - override suspend fun stop(context: RecordingStopContext): AppResult = measurement.stop() + override suspend fun stop(context: RecordingStopContext): AppResult = recording.stop() } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/AndroidRecordingSources.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/AndroidRecordingSources.kt index 1e3e3ed..351082b 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/AndroidRecordingSources.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/AndroidRecordingSources.kt @@ -10,35 +10,35 @@ import com.tomasrepcik.sensorbox.recording.RecordingSource import com.tomasrepcik.sensorbox.sensorservices.handlers.GPSHandler import com.tomasrepcik.sensorbox.sensorservices.handlers.MeasurementStorage import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.AndroidActivityRecognitionPlatform -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.GPSMeasurement -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SensorMeasurement -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SignificantMotion -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.GpsRecording +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SensorRecording +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SignificantMotionRecording +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest internal class AndroidRecordingSources( context: Context, - request: MeasurementLaunchRequest, + request: RecordingRequest, storage: MeasurementStorage, diagnosticLogger: DiagnosticLogger, clock: EpochClock, ) { private val sensorManager = context.getSystemService(SensorManager::class.java) - private val session = SessionRecordingSource(request, storage, clock, sensorManager) + private val sessionMetadata = SessionMetadataRecordingSource(request, storage, clock, sensorManager) val sources: List = listOf( - session, + sessionMetadata, SensorRecordingSource( request, - SensorMeasurement( + SensorRecording( storage = storage, diagnosticLogger = diagnosticLogger, sensorManager = sensorManager, - onStopped = session::recordSensorStats, + onStopped = sessionMetadata::recordSensorStats, ), ), GpsRecordingSource( request, - GPSMeasurement(GPSHandler(LocationServices.getFusedLocationProviderClient(context)), storage, clock), + GpsRecording(GPSHandler(LocationServices.getFusedLocationProviderClient(context)), storage, clock), ), ActivityRecordingSource( request, @@ -47,11 +47,11 @@ internal class AndroidRecordingSources( ), SignificantMotionRecordingSource( request, - SignificantMotion(storage, sensorManager), + SignificantMotionRecording(storage, sensorManager), ), ) - fun annotate(timestampMillis: Long, text: String): AppResult = session.annotate(timestampMillis, text) + fun annotate(timestampMillis: Long, text: String): AppResult = sessionMetadata.annotate(timestampMillis, text) - fun playAlarm(): AppResult = session.playAlarm() + fun playAlarm(): AppResult = sessionMetadata.playAlarm() } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/GpsRecordingSource.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/GpsRecordingSource.kt index e031e90..f5bb233 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/GpsRecordingSource.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/GpsRecordingSource.kt @@ -5,18 +5,16 @@ import com.tomasrepcik.sensorbox.recording.RecordingSource import com.tomasrepcik.sensorbox.recording.RecordingSourceSpec import com.tomasrepcik.sensorbox.recording.RecordingSourceType import com.tomasrepcik.sensorbox.recording.RecordingStopContext -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.GPSMeasurement -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.GpsRecording +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest -internal class GpsRecordingSource( - private val request: MeasurementLaunchRequest, - private val measurement: GPSMeasurement, -) : RecordingSource { +internal class GpsRecordingSource(private val request: RecordingRequest, private val recording: GpsRecording) : + RecordingSource { override val type = RecordingSourceType.GPS - override val failures = measurement.failures + override val failures = recording.failures override suspend fun start(spec: RecordingSourceSpec): AppResult = if (spec is RecordingSourceSpec.Gps) { - measurement.start( + recording.start( folderName = request.folderName, useInternalStorage = request.useInternalStorage, intervalSeconds = spec.intervalSeconds, @@ -26,5 +24,5 @@ internal class GpsRecordingSource( invalidSpec(type) } - override suspend fun stop(context: RecordingStopContext): AppResult = measurement.stop() + override suspend fun stop(context: RecordingStopContext): AppResult = recording.stop() } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SensorRecordingSource.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SensorRecordingSource.kt index 3cadc8c..42bd070 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SensorRecordingSource.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SensorRecordingSource.kt @@ -5,18 +5,16 @@ import com.tomasrepcik.sensorbox.recording.RecordingSource import com.tomasrepcik.sensorbox.recording.RecordingSourceSpec import com.tomasrepcik.sensorbox.recording.RecordingSourceType import com.tomasrepcik.sensorbox.recording.RecordingStopContext -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SensorMeasurement -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SensorRecording +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest -internal class SensorRecordingSource( - private val request: MeasurementLaunchRequest, - private val measurement: SensorMeasurement, -) : RecordingSource { +internal class SensorRecordingSource(private val request: RecordingRequest, private val recording: SensorRecording) : + RecordingSource { override val type = RecordingSourceType.SENSOR - override val failures = measurement.failures + override val failures = recording.failures override suspend fun start(spec: RecordingSourceSpec): AppResult = if (spec is RecordingSourceSpec.Sensors) { - measurement.start( + recording.start( folderName = request.folderName, useInternalStorage = request.useInternalStorage, sensorTypes = spec.sensorTypes, @@ -26,5 +24,5 @@ internal class SensorRecordingSource( invalidSpec(type) } - override suspend fun stop(context: RecordingStopContext): AppResult = measurement.stop() + override suspend fun stop(context: RecordingStopContext): AppResult = recording.stop() } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceController.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceController.kt index 15d4dcd..4a28f95 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceController.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceController.kt @@ -6,18 +6,18 @@ import com.tomasrepcik.sensorbox.core.error.DiagnosticLogger import com.tomasrepcik.sensorbox.core.time.EpochClock import com.tomasrepcik.sensorbox.recording.RecordingEngine import com.tomasrepcik.sensorbox.recording.RecordingEvent -import com.tomasrepcik.sensorbox.recording.RecordingPlan import com.tomasrepcik.sensorbox.recording.RecordingSessionId import com.tomasrepcik.sensorbox.recording.RecordingSourceSpec import com.tomasrepcik.sensorbox.recording.RecordingStopReason import com.tomasrepcik.sensorbox.sensorservices.handlers.MeasurementStorage -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharedFlow +import com.tomasrepcik.sensorbox.recording.RecordingRequest as EngineRecordingRequest internal class ServiceController( context: Context, - private val request: MeasurementLaunchRequest, + private val request: RecordingRequest, scope: CoroutineScope, storage: MeasurementStorage, diagnosticLogger: DiagnosticLogger, @@ -32,7 +32,7 @@ internal class ServiceController( val events: SharedFlow = engine.events - suspend fun start(): AppResult = engine.start(request.toRecordingPlan(sessionId)) + suspend fun start(): AppResult = engine.start(request.toEngineRequest(sessionId)) suspend fun stop(reason: RecordingStopReason): AppResult = engine.stop(reason) @@ -40,15 +40,15 @@ internal class ServiceController( fun playAlarm(): AppResult = androidSources.playAlarm() - private fun MeasurementLaunchRequest.toRecordingPlan(sessionId: RecordingSessionId): RecordingPlan { + private fun RecordingRequest.toEngineRequest(sessionId: RecordingSessionId): EngineRecordingRequest { val specs = buildList { - add(RecordingSourceSpec.Session) + add(RecordingSourceSpec.SessionMetadata) if (sensorIds.isNotEmpty()) add(RecordingSourceSpec.Sensors(sensorIds, sensorSamplingPeriod)) if (includesGps) add(RecordingSourceSpec.Gps(gpsIntervalSeconds, gpsMinDistanceMeters)) if (activityRecognition) add(RecordingSourceSpec.ActivityRecognition(activityRecognitionPeriodSeconds)) if (significantMotion) add(RecordingSourceSpec.SignificantMotion) } - return RecordingPlan( + return EngineRecordingRequest( sessionId = sessionId, sources = specs, durationMillis = durationMillis, diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceControllerFactory.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceControllerFactory.kt index b8855e1..4e2b6b2 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceControllerFactory.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/ServiceControllerFactory.kt @@ -4,7 +4,7 @@ import android.content.Context import com.tomasrepcik.sensorbox.core.error.DiagnosticLogger import com.tomasrepcik.sensorbox.core.time.EpochClock import com.tomasrepcik.sensorbox.sensorservices.handlers.StorageHandler -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest import kotlinx.coroutines.CoroutineScope import javax.inject.Inject @@ -13,7 +13,7 @@ internal class ServiceControllerFactory @Inject constructor( private val diagnosticLogger: DiagnosticLogger, private val clock: EpochClock, ) { - fun create(context: Context, request: MeasurementLaunchRequest, scope: CoroutineScope) = ServiceController( + fun create(context: Context, request: RecordingRequest, scope: CoroutineScope) = ServiceController( context = context, request = request, scope = scope, diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionRecordingSource.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionMetadataRecordingSource.kt similarity index 62% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionRecordingSource.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionMetadataRecordingSource.kt index 9cc31c0..831109d 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionRecordingSource.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SessionMetadataRecordingSource.kt @@ -13,55 +13,56 @@ import com.tomasrepcik.sensorbox.recording.RecordingSourceSpec import com.tomasrepcik.sensorbox.recording.RecordingSourceType import com.tomasrepcik.sensorbox.recording.RecordingStopContext import com.tomasrepcik.sensorbox.sensorservices.handlers.MeasurementStorage -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.ExtraInfoHandler -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.MeasurementMetadataWriter +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest import com.tomasrepcik.sensorbox.sensorservices.types.SensorFileStats -internal class SessionRecordingSource( - private val request: MeasurementLaunchRequest, +internal class SessionMetadataRecordingSource( + private val request: RecordingRequest, private val storage: MeasurementStorage, clock: EpochClock, sensorManager: SensorManager, ) : RecordingSource { - private val extraInfo = ExtraInfoHandler(storage, clock, sensorManager) + private val metadataWriter = MeasurementMetadataWriter(storage, clock, sensorManager) private var toneGenerator: ToneGenerator? = null - override val type = RecordingSourceType.SESSION + override val type = RecordingSourceType.SESSION_METADATA - override suspend fun start(spec: RecordingSourceSpec): AppResult = if (spec is RecordingSourceSpec.Session) { - storage.createMeasurementDirectory(request.folderName, request.useInternalStorage) - .onSuccess { extraInfo.start(request) } - } else { - invalidSpec(type) - } + override suspend fun start(spec: RecordingSourceSpec): AppResult = + if (spec is RecordingSourceSpec.SessionMetadata) { + storage.createMeasurementDirectory(request.folderName, request.useInternalStorage) + .onSuccess { metadataWriter.start(request) } + } else { + invalidSpec(type) + } fun annotate(timestampMillis: Long, text: String): AppResult = appResult( - AppErrorCode.MEASUREMENT, + AppErrorCode.RECORDING, "Add measurement annotation", ) { - extraInfo.annotate(timestampMillis, text) + metadataWriter.annotate(timestampMillis, text) } - fun playAlarm(): AppResult = appResult(AppErrorCode.MEASUREMENT, "Play measurement alarm") { - extraInfo.alarmTriggered() + fun playAlarm(): AppResult = appResult(AppErrorCode.RECORDING, "Play recording alarm") { + metadataWriter.alarmTriggered() val tone = toneGenerator ?: ToneGenerator(AudioManager.STREAM_ALARM, 100).also { toneGenerator = it } tone.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, ALARM_DURATION_MILLIS) } fun recordSensorStats(stats: List) { - extraInfo.recordSensorStats(stats) + metadataWriter.recordSensorStats(stats) } override suspend fun stop(context: RecordingStopContext): AppResult { - val releaseTone = appResult(AppErrorCode.MEASUREMENT, "Release measurement alarm") { + val releaseTone = appResult(AppErrorCode.RECORDING, "Release recording alarm") { toneGenerator?.release() toneGenerator = null } val metadataContext = releaseTone.errorOrNull() ?.let(context::withFailure) ?: context - return listOf(releaseTone, extraInfo.write(metadataContext)) - .combineAppResults(AppErrorCode.MEASUREMENT, "Close measurement session artifacts") + return listOf(releaseTone, metadataWriter.write(metadataContext)) + .combineAppResults(AppErrorCode.RECORDING, "Close recording session metadata") } private companion object { diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SignificantMotionRecordingSource.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SignificantMotionRecordingSource.kt index 59c3aca..af200fe 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SignificantMotionRecordingSource.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/serviceController/SignificantMotionRecordingSource.kt @@ -5,22 +5,22 @@ import com.tomasrepcik.sensorbox.recording.RecordingSource import com.tomasrepcik.sensorbox.recording.RecordingSourceSpec import com.tomasrepcik.sensorbox.recording.RecordingSourceType import com.tomasrepcik.sensorbox.recording.RecordingStopContext -import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SignificantMotion -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.handlers.measurements.SignificantMotionRecording +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest internal class SignificantMotionRecordingSource( - private val request: MeasurementLaunchRequest, - private val measurement: SignificantMotion, + private val request: RecordingRequest, + private val recording: SignificantMotionRecording, ) : RecordingSource { override val type = RecordingSourceType.SIGNIFICANT_MOTION - override val failures = measurement.failures + override val failures = recording.failures override suspend fun start(spec: RecordingSourceSpec): AppResult = if (spec is RecordingSourceSpec.SignificantMotion) { - measurement.start(request.folderName, request.useInternalStorage) + recording.start(request.folderName, request.useInternalStorage) } else { invalidSpec(type) } - override suspend fun stop(context: RecordingStopContext): AppResult = measurement.stop() + override suspend fun stop(context: RecordingStopContext): AppResult = recording.stop() } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/Notify.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/Notify.kt index 15a5440..5377fdd 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/Notify.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/Notify.kt @@ -13,8 +13,8 @@ import com.tomasrepcik.sensorbox.sensorservices.R object Notify { fun createRecordingNotification(context: Context): Notification { createChannel(context) - val stopIntent = Intent(context, MeasurementService::class.java) - .setAction(MeasurementService.ACTION_STOP) + val stopIntent = Intent(context, RecordingService::class.java) + .setAction(RecordingService.ACTION_STOP_RECORDING) val stopAction = PendingIntent.getService( context, STOP_REQUEST_CODE, @@ -52,7 +52,7 @@ object Notify { context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel) } - private const val CHANNEL_ID = "measurement" + private const val CHANNEL_ID = "recording" private const val OPEN_APP_REQUEST_CODE = 10 private const val STOP_REQUEST_CODE = 20 } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementHostResources.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingHostResources.kt similarity index 85% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementHostResources.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingHostResources.kt index dfe96d2..2c8a024 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementHostResources.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingHostResources.kt @@ -14,13 +14,13 @@ import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.appResult import com.tomasrepcik.sensorbox.core.error.combineAppResults import com.tomasrepcik.sensorbox.recording.RecordingStopReason -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -internal class MeasurementHostResources( +internal class RecordingHostResources( private val service: Service, private val scope: CoroutineScope, private val requestStop: (RecordingStopReason) -> Unit, @@ -36,7 +36,7 @@ internal class MeasurementHostResources( } } - fun start(request: MeasurementLaunchRequest) { + fun start(request: RecordingRequest) { promoteToForeground(request) if (request.requiresWakeLock) acquireWakeLock() if (request.stopOnLowBattery) registerLowBatteryReceiver() @@ -55,14 +55,14 @@ internal class MeasurementHostResources( alarmJobs.forEach(Job::cancel) alarmJobs = emptyList() return listOf(releaseWakeLock(), unregisterLowBatteryReceiver()) - .combineAppResults(AppErrorCode.MEASUREMENT, "Release recording host resources") + .combineAppResults(AppErrorCode.RECORDING, "Release recording host resources") } fun removeNotification() { service.stopForeground(Service.STOP_FOREGROUND_REMOVE) } - private fun promoteToForeground(request: MeasurementLaunchRequest) { + private fun promoteToForeground(request: RecordingRequest) { val notification = Notify.createRecordingNotification(service) if (Build.VERSION.SDK_INT >= 34) { service.startForeground(NOTIFICATION_ID, notification, foregroundTypes(request)) @@ -71,7 +71,7 @@ internal class MeasurementHostResources( } } - private fun foregroundTypes(request: MeasurementLaunchRequest): Int = if (request.includesGps) { + private fun foregroundTypes(request: RecordingRequest): Int = if (request.includesGps) { FOREGROUND_SERVICE_TYPE_HEALTH or FOREGROUND_SERVICE_TYPE_LOCATION } else { FOREGROUND_SERVICE_TYPE_HEALTH @@ -93,15 +93,15 @@ internal class MeasurementHostResources( } private fun releaseWakeLock(): AppResult = appResult( - AppErrorCode.MEASUREMENT, - "Release measurement wake lock", + AppErrorCode.RECORDING, + "Release recording wake lock", ) { wakeLock?.takeIf(PowerManager.WakeLock::isHeld)?.release() wakeLock = null } private fun unregisterLowBatteryReceiver(): AppResult = appResult( - AppErrorCode.MEASUREMENT, + AppErrorCode.RECORDING, "Unregister low battery receiver", ) { if (batteryReceiverRegistered) service.unregisterReceiver(lowBatteryReceiver) @@ -110,6 +110,6 @@ internal class MeasurementHostResources( private companion object { const val NOTIFICATION_ID = 729 - const val WAKE_LOCK_TAG = "SensorBox::Measurement" + const val WAKE_LOCK_TAG = "SensorBox::Recording" } } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementService.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingService.kt similarity index 75% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementService.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingService.kt index 10354ea..908823e 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/MeasurementService.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/services/RecordingService.kt @@ -11,12 +11,12 @@ import com.tomasrepcik.sensorbox.core.error.combineAppResults import com.tomasrepcik.sensorbox.core.time.EpochClock import com.tomasrepcik.sensorbox.recording.RecordingEvent import com.tomasrepcik.sensorbox.recording.RecordingStopReason -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest import com.tomasrepcik.sensorbox.sensorservices.serviceController.ServiceController import com.tomasrepcik.sensorbox.sensorservices.serviceController.ServiceControllerFactory -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionStore -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementStopReason +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStopReason +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStore import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -28,9 +28,9 @@ import kotlinx.coroutines.runBlocking import javax.inject.Inject @AndroidEntryPoint -class MeasurementService : Service() { +class RecordingService : Service() { @Inject - lateinit var sessionStore: MeasurementSessionStore + lateinit var sessionStore: RecordingSessionStore @Inject internal lateinit var controllerFactory: ServiceControllerFactory @@ -40,11 +40,11 @@ class MeasurementService : Service() { private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private var controller: ServiceController? = null - private var activeRequest: MeasurementLaunchRequest? = null + private var activeRequest: RecordingRequest? = null private var eventJob: Job? = null private var isFinishing = false private val hostResources by lazy { - MeasurementHostResources( + RecordingHostResources( service = this, scope = serviceScope, requestStop = ::requestStop, @@ -56,7 +56,7 @@ class MeasurementService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { - ACTION_STOP -> requestStop(RecordingStopReason.USER_REQUEST) + ACTION_STOP_RECORDING -> requestStop(RecordingStopReason.USER_REQUEST) ACTION_ANNOTATE -> controller?.annotate( intent.getLongExtra(ANNOTATION_TIME, epochClock.nowMillis()), @@ -69,8 +69,8 @@ class MeasurementService : Service() { } private fun startRecordingHost(intent: Intent) { - appResult(AppErrorCode.MEASUREMENT, "Start recording foreground host") { - val request = MeasurementLaunchRequest.from(intent) + appResult(AppErrorCode.RECORDING, "Start recording foreground host") { + val request = RecordingRequest.from(intent) require(request.sessionId.isNotBlank()) { "Recording session ID is missing" } activeRequest = request isFinishing = false @@ -82,7 +82,7 @@ class MeasurementService : Service() { val result = serviceController.start() if (result.isFailure) { finishRecording( - MeasurementStopReason.SOURCE_FAILURE, + RecordingSessionStopReason.SOURCE_FAILURE, result, ) } @@ -90,7 +90,7 @@ class MeasurementService : Service() { }.onFailure { error -> serviceScope.launch { finishRecording( - MeasurementStopReason.SOURCE_FAILURE, + RecordingSessionStopReason.SOURCE_FAILURE, AppResult.failure(error), ) } @@ -105,7 +105,7 @@ class MeasurementService : Service() { is RecordingEvent.RecordingStarted -> onRecordingStarted(event) is RecordingEvent.RecordingStopped -> finishRecording( - event.reason.toMeasurementReason(), + event.reason.toRecordingSessionReason(), event.result, ) } @@ -116,7 +116,7 @@ class MeasurementService : Service() { private fun onRecordingStarted(event: RecordingEvent.RecordingStarted) { val request = activeRequest ?: return sessionStore.markRunning( - MeasurementSessionState.Running( + RecordingSessionState.Running( sessionId = event.sessionId.value, folderName = request.folderName, startedAtElapsedRealtime = SystemClock.elapsedRealtime(), @@ -135,13 +135,13 @@ class MeasurementService : Service() { } } - private suspend fun finishRecording(reason: MeasurementStopReason, engineResult: AppResult) { + private suspend fun finishRecording(reason: RecordingSessionStopReason, engineResult: AppResult) { if (isFinishing) return isFinishing = true val sessionId = activeRequest?.sessionId.orEmpty() val hostResult = finishHost() val result = listOf(engineResult, hostResult) - .combineAppResults(AppErrorCode.MEASUREMENT, "Finish recording foreground host") + .combineAppResults(AppErrorCode.RECORDING, "Finish recording foreground host") sessionStore.publishStopped(sessionId, reason, result) eventJob?.cancel() eventJob = null @@ -152,16 +152,16 @@ class MeasurementService : Service() { results += hostResources.release() activeRequest = null controller = null - results += appResult(AppErrorCode.MEASUREMENT, "Publish idle measurement state") { + results += appResult(AppErrorCode.RECORDING, "Publish idle recording state") { sessionStore.markIdle() } - results += appResult(AppErrorCode.MEASUREMENT, "Remove measurement notification") { + results += appResult(AppErrorCode.RECORDING, "Remove recording notification") { hostResources.removeNotification() } - results += appResult(AppErrorCode.MEASUREMENT, "Stop measurement service instance") { + results += appResult(AppErrorCode.RECORDING, "Stop recording service instance") { stopSelf() } - return results.combineAppResults(AppErrorCode.MEASUREMENT, "Finish recording host resources") + return results.combineAppResults(AppErrorCode.RECORDING, "Finish recording host resources") } override fun onDestroy() { @@ -175,9 +175,9 @@ class MeasurementService : Service() { sessionStore.markIdle() sessionStore.publishStopped( request.sessionId, - MeasurementStopReason.SERVICE_DESTROYED, + RecordingSessionStopReason.SERVICE_DESTROYED, listOf(cleanup, resources).combineAppResults( - AppErrorCode.MEASUREMENT, + AppErrorCode.RECORDING, "Destroy recording foreground host", ), ) @@ -187,12 +187,12 @@ class MeasurementService : Service() { super.onDestroy() } - private fun RecordingStopReason.toMeasurementReason(): MeasurementStopReason = when (this) { - RecordingStopReason.USER_REQUEST -> MeasurementStopReason.USER_REQUEST - RecordingStopReason.DURATION_EXPIRED -> MeasurementStopReason.DURATION_EXPIRED - RecordingStopReason.LOW_BATTERY -> MeasurementStopReason.LOW_BATTERY - RecordingStopReason.SOURCE_FAILURE -> MeasurementStopReason.SOURCE_FAILURE - RecordingStopReason.PLATFORM_DESTROYED -> MeasurementStopReason.SERVICE_DESTROYED + private fun RecordingStopReason.toRecordingSessionReason(): RecordingSessionStopReason = when (this) { + RecordingStopReason.USER_REQUEST -> RecordingSessionStopReason.USER_REQUEST + RecordingStopReason.DURATION_EXPIRED -> RecordingSessionStopReason.DURATION_EXPIRED + RecordingStopReason.LOW_BATTERY -> RecordingSessionStopReason.LOW_BATTERY + RecordingStopReason.SOURCE_FAILURE -> RecordingSessionStopReason.SOURCE_FAILURE + RecordingStopReason.PLATFORM_DESTROYED -> RecordingSessionStopReason.SERVICE_DESTROYED } companion object { @@ -215,6 +215,6 @@ class MeasurementService : Service() { const val ACTION_ANNOTATE = "com.tomasrepcik.sensorbox.action.ANNOTATE_MEASUREMENT" const val ANNOTATION_TIME = "ANNOTATION_TIME" const val ANNOTATION_TEXT = "ANNOTATION_TEXT" - const val ACTION_STOP = "com.tomasrepcik.sensorbox.action.STOP_MEASUREMENT" + const val ACTION_STOP_RECORDING = "com.tomasrepcik.sensorbox.action.STOP_RECORDING" } } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/MeasurementSessionStore.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/MeasurementSessionStore.kt deleted file mode 100644 index f93fbb5..0000000 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/MeasurementSessionStore.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.tomasrepcik.sensorbox.sensorservices.session - -import com.tomasrepcik.sensorbox.core.error.AppResult -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class MeasurementSessionStore @Inject constructor() { - private val mutableState = MutableStateFlow(MeasurementSessionState.Idle) - private val mutableEvents = MutableSharedFlow(extraBufferCapacity = EVENT_BUFFER_SIZE) - val state: StateFlow = mutableState.asStateFlow() - val events: SharedFlow = mutableEvents - - fun markRunning(state: MeasurementSessionState.Running) { - mutableState.value = state - } - - fun markStopping() { - mutableState.value = MeasurementSessionState.Stopping - } - - fun markIdle() { - mutableState.value = MeasurementSessionState.Idle - } - - fun publishStopped(sessionId: String, reason: MeasurementStopReason, result: AppResult) { - mutableEvents.tryEmit(MeasurementStopped(sessionId, reason, result)) - } - - private companion object { - const val EVENT_BUFFER_SIZE = 16 - } -} - -enum class MeasurementStopReason { - USER_REQUEST, - DURATION_EXPIRED, - LOW_BATTERY, - SOURCE_FAILURE, - SERVICE_DESTROYED, -} - -data class MeasurementStopped(val sessionId: String, val reason: MeasurementStopReason, val result: AppResult) diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/MeasurementSessionState.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/RecordingSessionState.kt similarity index 60% rename from sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/MeasurementSessionState.kt rename to sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/RecordingSessionState.kt index 3c0b7c3..fa3b3c7 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/MeasurementSessionState.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/RecordingSessionState.kt @@ -1,7 +1,7 @@ package com.tomasrepcik.sensorbox.sensorservices.session -sealed interface MeasurementSessionState { - data object Idle : MeasurementSessionState +sealed interface RecordingSessionState { + data object Idle : RecordingSessionState data class Running( val sessionId: String, @@ -9,7 +9,7 @@ sealed interface MeasurementSessionState { val startedAtElapsedRealtime: Long, val sensorIds: List, val includesGps: Boolean, - ) : MeasurementSessionState + ) : RecordingSessionState - data object Stopping : MeasurementSessionState + data object Stopping : RecordingSessionState } diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/RecordingSessionStore.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/RecordingSessionStore.kt new file mode 100644 index 0000000..752b71f --- /dev/null +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/session/RecordingSessionStore.kt @@ -0,0 +1,52 @@ +package com.tomasrepcik.sensorbox.sensorservices.session + +import com.tomasrepcik.sensorbox.core.error.AppResult +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RecordingSessionStore @Inject constructor() { + private val mutableState = MutableStateFlow(RecordingSessionState.Idle) + private val mutableEvents = MutableSharedFlow(extraBufferCapacity = EVENT_BUFFER_SIZE) + val state: StateFlow = mutableState.asStateFlow() + val events: SharedFlow = mutableEvents + + fun markRunning(state: RecordingSessionState.Running) { + mutableState.value = state + } + + fun markStopping() { + mutableState.value = RecordingSessionState.Stopping + } + + fun markIdle() { + mutableState.value = RecordingSessionState.Idle + } + + fun publishStopped(sessionId: String, reason: RecordingSessionStopReason, result: AppResult) { + mutableEvents.tryEmit(RecordingSessionStopped(sessionId, reason, result)) + } + + private companion object { + const val EVENT_BUFFER_SIZE = 16 + } +} + +enum class RecordingSessionStopReason { + USER_REQUEST, + DURATION_EXPIRED, + LOW_BATTERY, + SOURCE_FAILURE, + SERVICE_DESTROYED, +} + +data class RecordingSessionStopped( + val sessionId: String, + val reason: RecordingSessionStopReason, + val result: AppResult, +) diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorHolder.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorHolder.kt index 8e2a419..05c9322 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorHolder.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorHolder.kt @@ -55,7 +55,7 @@ internal class SensorHolder( } else if (result.exceptionOrNull() == null) { droppedSamples.incrementAndGet() val cause = IllegalStateException("Sensor sample buffer is full") - val error = AppError.from(AppErrorCode.MEASUREMENT, "Buffer ${spec.fileName}", cause) + val error = AppError.from(AppErrorCode.RECORDING, "Buffer ${spec.fileName}", cause) reportFailure(error) samples.close(cause) } else if (failure.get() != null) { diff --git a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorSpec.kt b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorSpec.kt index 1488b85..f1150d5 100644 --- a/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorSpec.kt +++ b/sensorservices/src/main/java/com/tomasrepcik/sensorbox/sensorservices/types/SensorSpec.kt @@ -32,7 +32,7 @@ enum class SensorSpec(val type: Int, val axisCount: Int, val fileName: String, v STEP_COUNTER(Sensor.TYPE_STEP_COUNTER, 1, "step_counter.csv", "t_sensor;steps;accuracy\n"), STEP_DETECTOR(Sensor.TYPE_STEP_DETECTOR, 1, "step_detector.csv", "t_sensor;step;accuracy\n"), - // Kept discoverable in the sensor catalogue, but recorded by SignificantMotion's trigger listener. + // Kept discoverable in the sensor catalogue, but recorded by SignificantMotionRecording's trigger listener. SIGNIFICANT_MOTION( Sensor.TYPE_SIGNIFICANT_MOTION, 1, diff --git a/sensorservices/src/main/res/values/strings.xml b/sensorservices/src/main/res/values/strings.xml index 2acf622..c156a13 100644 --- a/sensorservices/src/main/res/values/strings.xml +++ b/sensorservices/src/main/res/values/strings.xml @@ -4,6 +4,6 @@ No storage path Stop Recording sensors - SensorBox is writing measurement data + SensorBox is recording sensor data Sensor recording diff --git a/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionMeasurementTest.kt b/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionRecordingTest.kt similarity index 90% rename from sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionMeasurementTest.kt rename to sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionRecordingTest.kt index 3520139..6e7424a 100644 --- a/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionMeasurementTest.kt +++ b/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/ActivityRecognitionRecordingTest.kt @@ -11,16 +11,16 @@ import org.junit.Test import java.io.ByteArrayOutputStream import java.io.OutputStream -class ActivityRecognitionMeasurementTest { +class ActivityRecognitionRecordingTest { @Test - fun `Given fake storage and platform When activity measurement runs Then lifecycle has no Android types`() = + fun `Given fake storage and platform When activity recording runs Then lifecycle has no Android types`() = runBlocking { val storage = FakeMeasurementStorage() val platform = FakeActivityRecognitionPlatform() - val measurement = ActivityRecognitionMeasurement(storage, platform) + val recording = ActivityRecognitionRecording(storage, platform) assertTrue( - measurement.start( + recording.start( folderName = "session", useInternalStorage = true, periodSeconds = 15, @@ -28,7 +28,7 @@ class ActivityRecognitionMeasurementTest { ) platform.emitUpdate(ActivityUpdate(123L, listOf(1, 2, 3, 4, 5, 6, 7, 8))) platform.emitTransitions(listOf(ActivityTransitionSample(456L, 2, 1))) - val stopped = measurement.stop() + val stopped = recording.stop() assertTrue(stopped.isSuccess) assertEquals(15, platform.startedPeriodSeconds) @@ -41,9 +41,9 @@ class ActivityRecognitionMeasurementTest { fun `Given transition storage failure When started Then platform initialization does not run`() = runBlocking { val storage = FakeMeasurementStorage(failOnCall = 2) val platform = FakeActivityRecognitionPlatform() - val measurement = ActivityRecognitionMeasurement(storage, platform) + val recording = ActivityRecognitionRecording(storage, platform) - val result = measurement.start( + val result = recording.start( folderName = "session", useInternalStorage = false, periodSeconds = 15, diff --git a/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/MeasurementPreparationTest.kt b/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/RecordingSourceStartTest.kt similarity index 88% rename from sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/MeasurementPreparationTest.kt rename to sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/RecordingSourceStartTest.kt index 1f5eadd..4cc244d 100644 --- a/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/MeasurementPreparationTest.kt +++ b/sensorservices/src/test/java/com/tomasrepcik/sensorbox/sensorservices/handlers/measurements/RecordingSourceStartTest.kt @@ -13,13 +13,13 @@ import org.junit.Assert.assertTrue import org.junit.Test import java.io.OutputStream -class MeasurementPreparationTest { +class RecordingSourceStartTest { @Test fun `Given first sensor file failure When sensors start Then later sensor files are not opened`() { val storage = FailingMeasurementStorage() - val measurement = SensorMeasurement(storage, DiagnosticLogger { }) + val recording = SensorRecording(storage, DiagnosticLogger { }) - val result = measurement.start( + val result = recording.start( folderName = "session", useInternalStorage = true, sensorTypes = setOf(Sensor.TYPE_ACCELEROMETER, Sensor.TYPE_GYROSCOPE), @@ -33,9 +33,9 @@ class MeasurementPreparationTest { @Test fun `Given GPS file failure When GPS starts Then no output work follows`() { val storage = FailingMeasurementStorage() - val measurement = GPSMeasurement(GPSHandler(), storage, EpochClock { 1L }) + val recording = GpsRecording(GPSHandler(), storage, EpochClock { 1L }) - val result = measurement.start( + val result = recording.start( folderName = "session", useInternalStorage = true, intervalSeconds = 10, diff --git a/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearRecordingEmulatorFixture.kt b/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearRecordingEmulatorFixture.kt index 26749f0..0925a67 100644 --- a/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearRecordingEmulatorFixture.kt +++ b/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearRecordingEmulatorFixture.kt @@ -9,8 +9,8 @@ import androidx.test.platform.app.InstrumentationRegistry import com.tomasrepcik.sensorbox.core.preferences.AppPreferences import com.tomasrepcik.sensorbox.core.preferences.RecordingPreferences import com.tomasrepcik.sensorbox.core.time.SystemEpochClock -import com.tomasrepcik.sensorbox.domain.measurement.WearMeasurementControlUseCase -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementIntentFactory +import com.tomasrepcik.sensorbox.domain.recording.DefaultWatchRecordingControlUseCase +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingIntentFactory import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -46,15 +46,15 @@ internal data class WearTestLocation( internal class WearRecordingEmulatorFixture(private val context: Context) { private val instrumentation = InstrumentationRegistry.getInstrumentation() private val sensorManager = context.getSystemService(SensorManager::class.java) - private val controller = WearMeasurementControlUseCase( + private val controller = DefaultWatchRecordingControlUseCase( context = context, - intentFactory = MeasurementIntentFactory(context, SystemEpochClock), + intentFactory = RecordingIntentFactory(context, SystemEpochClock), ) private var mockGpsActive = false fun prepareDevice() { assumeTrue( - "Wear recording scenarios require Wear OS", + "watch recording scenarios require Wear OS", context.packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH), ) grantRuntimePermissions() @@ -86,7 +86,7 @@ internal class WearRecordingEmulatorFixture(private val context: Context) { gpsIntervalSeconds = scenario.gpsIntervalSeconds, gpsMinDistanceMeters = scenario.gpsMinDistanceMeters, sensorSamplingPeriod = scenario.samplingIndex, - restrictMeasurementOnLowBattery = scenario.stopOnLowBattery, + stopRecordingOnLowBattery = scenario.stopOnLowBattery, useWakeLock = scenario.useWakeLock, ), ) @@ -184,7 +184,7 @@ internal class WearRecordingEmulatorFixture(private val context: Context) { output.takeIf { it.isFile && it.length() > 0L }?.let { return JSONObject(it.readText()) } Thread.sleep(FILE_WAIT_INTERVAL_MILLIS) } - assertTrue("Wear recording did not finalize $output", output.isFile) + assertTrue("watch recording did not finalize $output", output.isFile) return JSONObject(output.readText()) } diff --git a/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearToPhoneSyncSenderEmulatorTest.kt b/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearToPhoneSyncSenderEmulatorTest.kt index 8a017b8..4de612d 100644 --- a/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearToPhoneSyncSenderEmulatorTest.kt +++ b/wear/src/androidTest/java/com/tomasrepcik/sensorbox/emulator/WearToPhoneSyncSenderEmulatorTest.kt @@ -9,7 +9,7 @@ import com.google.android.gms.wearable.CapabilityClient import com.google.android.gms.wearable.Wearable import com.tomasrepcik.sensorbox.core.error.AppError import com.tomasrepcik.sensorbox.core.error.AppResult -import com.tomasrepcik.sensorbox.domain.sync.SyncWearMeasurementsUseCase +import com.tomasrepcik.sensorbox.domain.sync.SyncWatchMeasurementsUseCase import com.tomasrepcik.sensorbox.wearoslib.connectivity.GooglePlayWearConnectionRepository import com.tomasrepcik.sensorbox.wearoslib.files.GooglePlayWearFileTransferClient import com.tomasrepcik.sensorbox.wearoslib.files.WearSyncEmulatorFixture @@ -44,7 +44,7 @@ class WearToPhoneSyncSenderEmulatorTest { @Test fun givenPairedEmulatorsWhenWearSyncsThenEverySupportedFileIsTransferred() = runBlocking { - val sync = SyncWearMeasurementsUseCase( + val sync = SyncWatchMeasurementsUseCase( context = context, connectionRepository = GooglePlayWearConnectionRepository(context), transferClient = GooglePlayWearFileTransferClient(context), @@ -53,7 +53,7 @@ class WearToPhoneSyncSenderEmulatorTest { assertEquals(scenario.transferredFiles.size, syncWhenPhoneBecomesReachable(sync)) } - private suspend fun syncWhenPhoneBecomesReachable(sync: SyncWearMeasurementsUseCase): Int { + private suspend fun syncWhenPhoneBecomesReachable(sync: SyncWatchMeasurementsUseCase): Int { var lastFailure: AppError? = null repeat(MAX_ATTEMPTS) { when (val result = sync()) { diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/PhoneRecordingResultInbox.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/PhoneRecordingResultInbox.kt index b642275..152ffe8 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/PhoneRecordingResultInbox.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/PhoneRecordingResultInbox.kt @@ -1,7 +1,7 @@ package com.tomasrepcik.sensorbox.communication import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import java.util.concurrent.ConcurrentHashMap @@ -18,19 +18,19 @@ class PhoneRecordingResultInbox @Inject constructor() { updates.tryEmit(result) } - fun clear(sessionId: String, action: WearRecordingAction) { - results.remove(Key(sessionId, action)) + fun clear(sessionId: String, operation: WearRecordingOperation) { + results.remove(Key(sessionId, operation)) } - suspend fun await(sessionId: String, action: WearRecordingAction): WearCommand.RecordingResult { - val key = Key(sessionId, action) + suspend fun await(sessionId: String, operation: WearRecordingOperation): WearCommand.RecordingResult { + val key = Key(sessionId, operation) results[key]?.let { return it } return updates.first { result -> result.key() == key } } - private fun WearCommand.RecordingResult.key() = Key(sessionId, action) + private fun WearCommand.RecordingResult.key() = Key(sessionId, operation) - private data class Key(val sessionId: String, val action: WearRecordingAction) + private data class Key(val sessionId: String, val operation: WearRecordingOperation) private companion object { const val BUFFER_SIZE = 16 diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandEnvironment.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandEnvironment.kt index 8fcd36f..3bcc0ac 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandEnvironment.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandEnvironment.kt @@ -5,9 +5,9 @@ import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.preferences.AppPreferences import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesRepository -import com.tomasrepcik.sensorbox.domain.measurement.WearMeasurementPermissionUseCase -import com.tomasrepcik.sensorbox.domain.sensors.GetWearSensorsUseCase -import com.tomasrepcik.sensorbox.domain.sensors.toWearSensorInfo +import com.tomasrepcik.sensorbox.domain.recording.WatchRecordingPermissionUseCase +import com.tomasrepcik.sensorbox.domain.sensors.GetWatchSensorsUseCase +import com.tomasrepcik.sensorbox.domain.sensors.toWatchSensorInfo import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingRequest import com.tomasrepcik.sensorbox.wearoslib.protocol.WearSensorInfo import kotlinx.coroutines.flow.first @@ -20,22 +20,22 @@ interface WearRecordingRequirementsUseCase { } class WearRecordingEnvironment @Inject constructor( - private val measurementPermissions: WearMeasurementPermissionUseCase, + private val recordingPermissions: WatchRecordingPermissionUseCase, private val preferencesRepository: AppPreferencesRepository, - private val getWearSensors: GetWearSensorsUseCase, + private val getWatchSensors: GetWatchSensorsUseCase, ) : WearRecordingRequirementsUseCase { override suspend fun validate(request: WearRecordingRequest): AppResult { - val availableSensorIds = getWearSensors().map { it.type }.toSet() + val availableSensorIds = getWatchSensors().map { it.type }.toSet() if (!availableSensorIds.containsAll(request.sensorIds)) { - return AppResult.failure(AppError(AppErrorCode.VALIDATION, "Validate Wear recording sensors")) + return AppResult.failure(AppError(AppErrorCode.VALIDATION, "Validate watch recording sensors")) } - val missingPermissions = measurementPermissions(request.includesGps) + val missingPermissions = recordingPermissions(request.includesGps) if (missingPermissions.isNotEmpty()) { return AppResult.failure( AppError( code = AppErrorCode.PERMISSION, - operation = "Validate Wear recording permissions", + operation = "Validate watch recording permissions", diagnosticMessage = "Wear OS is missing required recording permissions", context = mapOf("missingPermissions" to missingPermissions.sorted().joinToString()), ), @@ -45,5 +45,7 @@ class WearRecordingEnvironment @Inject constructor( return preferencesRepository.preferences.first() } - override fun availableSensors(): List = getWearSensors().map { sensor -> sensor.toWearSensorInfo() } + override fun availableSensors(): List = getWatchSensors().map { sensor -> + sensor.toWatchSensorInfo() + } } diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandHandler.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandHandler.kt index ab1aef2..1b1bdb4 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandHandler.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearCommandHandler.kt @@ -3,19 +3,19 @@ package com.tomasrepcik.sensorbox.communication import com.tomasrepcik.sensorbox.core.error.AppError import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult -import com.tomasrepcik.sensorbox.domain.measurement.WearRecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.WatchRecordingControlUseCase import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.PHONE_APP_CAPABILITY import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.PHONE_MESSAGE_PATH import com.tomasrepcik.sensorbox.wearoslib.protocol.SendWearCommandUseCase import com.tomasrepcik.sensorbox.wearoslib.protocol.WearCommand -import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingAction +import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOperation import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingOutcome import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import kotlinx.coroutines.withTimeoutOrNull import javax.inject.Inject class WearCommandHandler @Inject constructor( - private val recording: WearRecordingControlUseCase, + private val recording: WatchRecordingControlUseCase, private val environment: WearRecordingRequirementsUseCase, private val sendCommand: SendWearCommandUseCase, private val phoneResults: PhoneRecordingResultInbox, @@ -45,7 +45,7 @@ class WearCommandHandler @Inject constructor( val sessionId = activeSessionId ?: return AppResult.success(Unit) activeSessionId = null val command = WearCommand.StopRecording(sessionId, reason) - phoneResults.clear(sessionId, WearRecordingAction.STOP) + phoneResults.clear(sessionId, WearRecordingOperation.STOP) var lastSendError: AppError? = null repeat(ATTEMPT_COUNT) { retryCount -> @@ -55,7 +55,7 @@ class WearCommandHandler @Inject constructor( is AppResult.Success -> { lastSendError = null val result = withTimeoutOrNull(RESULT_TIMEOUT_MILLIS / ATTEMPT_COUNT) { - phoneResults.await(sessionId, WearRecordingAction.STOP) + phoneResults.await(sessionId, WearRecordingOperation.STOP) } if (result != null) return result.toAppResult(retryCount) } @@ -77,7 +77,7 @@ class WearCommandHandler @Inject constructor( } private suspend fun start(command: WearCommand.StartRecording): WearCommand.RecordingResult { - cached(command.sessionId, WearRecordingAction.START)?.let { return it } + cached(command.sessionId, WearRecordingOperation.START)?.let { return it } val result = when (val active = activeSessionId) { command.sessionId -> AppResult.success(Unit) @@ -97,18 +97,18 @@ class WearCommandHandler @Inject constructor( else -> AppResult.failure( AppError( code = AppErrorCode.CONFLICT, - operation = "Start Wear recording", - diagnosticMessage = "Another Wear recording is active", + operation = "Start watch recording", + diagnosticMessage = "Another watch recording is active", context = mapOf("activeSessionId" to active), ), ) } - return remember(command.sessionId, WearRecordingAction.START, result) + return remember(command.sessionId, WearRecordingOperation.START, result) } private fun stop(command: WearCommand.StopRecording): WearCommand.RecordingResult { - cached(command.sessionId, WearRecordingAction.STOP)?.let { return it } + cached(command.sessionId, WearRecordingOperation.STOP)?.let { return it } val result = if (activeSessionId == command.sessionId) { recording.stop(command.sessionId, command.reason).onSuccess { @@ -118,7 +118,7 @@ class WearCommandHandler @Inject constructor( AppResult.success(Unit) } - return remember(command.sessionId, WearRecordingAction.STOP, result) + return remember(command.sessionId, WearRecordingOperation.STOP, result) } private suspend fun sendAvailableSensors(): AppResult = sendCommand( @@ -135,24 +135,24 @@ class WearCommandHandler @Inject constructor( private fun remember( sessionId: String, - action: WearRecordingAction, + operation: WearRecordingOperation, result: AppResult, ): WearCommand.RecordingResult { val error = result.errorOrNull() return WearCommand.RecordingResult( sessionId = sessionId, - action = action, + operation = operation, outcome = if (result.isSuccess) WearRecordingOutcome.SUCCEEDED else WearRecordingOutcome.FAILED, errorCode = error?.code, errorOperation = error?.operation, errorMessage = error?.diagnosticMessage, errorContext = error?.context.orEmpty(), failureCount = if (result.isFailure) 1 else 0, - ).also { completedResults[ResultKey(sessionId, action)] = it } + ).also { completedResults[ResultKey(sessionId, operation)] = it } } - private fun cached(sessionId: String, action: WearRecordingAction): WearCommand.RecordingResult? = - completedResults[ResultKey(sessionId, action)] + private fun cached(sessionId: String, operation: WearRecordingOperation): WearCommand.RecordingResult? = + completedResults[ResultKey(sessionId, operation)] private fun WearCommand.RecordingResult.toAppResult(retryCount: Int): AppResult = if (outcome == WearRecordingOutcome.SUCCEEDED) { @@ -161,8 +161,8 @@ class WearCommandHandler @Inject constructor( AppResult.failure( AppError( code = errorCode ?: AppErrorCode.UNKNOWN, - operation = errorOperation ?: "Handle phone $action result", - diagnosticMessage = errorMessage ?: "Phone $action failed", + operation = errorOperation ?: "Handle phone $operation result", + diagnosticMessage = errorMessage ?: "Phone $operation failed", context = errorContext + mapOf( "source" to "phone", "sessionId" to sessionId, @@ -173,7 +173,7 @@ class WearCommandHandler @Inject constructor( ) } - private data class ResultKey(val sessionId: String, val action: WearRecordingAction) + private data class ResultKey(val sessionId: String, val operation: WearRecordingOperation) private companion object { const val RESULT_TIMEOUT_MILLIS = 5_000L diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearRecordingSessionObserver.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearRecordingSessionObserver.kt index 2a99b1b..16ace59 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearRecordingSessionObserver.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/communication/WearRecordingSessionObserver.kt @@ -2,9 +2,9 @@ package com.tomasrepcik.sensorbox.communication import com.tomasrepcik.sensorbox.core.error.DiagnosticLogger import com.tomasrepcik.sensorbox.core.error.toDiagnosticEvent -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionStore -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementStopReason -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementStopped +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStopReason +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStopped +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStore import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -17,7 +17,7 @@ import javax.inject.Singleton @Singleton class WearRecordingSessionObserver @Inject constructor( - private val sessionStore: MeasurementSessionStore, + private val sessionStore: RecordingSessionStore, private val commandHandler: WearCommandHandler, private val diagnosticLogger: DiagnosticLogger, ) { @@ -31,19 +31,19 @@ class WearRecordingSessionObserver @Inject constructor( } } - private suspend fun onStopped(event: MeasurementStopped) { + private suspend fun onStopped(event: RecordingSessionStopped) { event.result.errorOrNull()?.let { error -> diagnosticLogger.record(error.toDiagnosticEvent()) } - val reason = event.reason.toAutomaticWearReason() ?: return + val reason = event.reason.toAutomaticWatchReason() ?: return commandHandler.onAutomaticStop(reason).onFailure { error -> diagnosticLogger.record(error.toDiagnosticEvent()) } } - private fun MeasurementStopReason.toAutomaticWearReason(): WearStopReason? = when (this) { - MeasurementStopReason.USER_REQUEST -> null - MeasurementStopReason.DURATION_EXPIRED -> WearStopReason.DURATION_EXPIRED - MeasurementStopReason.LOW_BATTERY -> WearStopReason.LOW_BATTERY - MeasurementStopReason.SOURCE_FAILURE -> WearStopReason.SOURCE_FAILURE - MeasurementStopReason.SERVICE_DESTROYED -> WearStopReason.SERVICE_DESTROYED + private fun RecordingSessionStopReason.toAutomaticWatchReason(): WearStopReason? = when (this) { + RecordingSessionStopReason.USER_REQUEST -> null + RecordingSessionStopReason.DURATION_EXPIRED -> WearStopReason.DURATION_EXPIRED + RecordingSessionStopReason.LOW_BATTERY -> WearStopReason.LOW_BATTERY + RecordingSessionStopReason.SOURCE_FAILURE -> WearStopReason.SOURCE_FAILURE + RecordingSessionStopReason.SERVICE_DESTROYED -> WearStopReason.SERVICE_DESTROYED } } diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt index 461469a..53cdc6b 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/di/RecordingModule.kt @@ -2,8 +2,8 @@ package com.tomasrepcik.sensorbox.di import com.tomasrepcik.sensorbox.communication.WearRecordingEnvironment import com.tomasrepcik.sensorbox.communication.WearRecordingRequirementsUseCase -import com.tomasrepcik.sensorbox.domain.measurement.WearMeasurementControlUseCase -import com.tomasrepcik.sensorbox.domain.measurement.WearRecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.DefaultWatchRecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.WatchRecordingControlUseCase import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -13,9 +13,9 @@ import dagger.hilt.components.SingletonComponent @InstallIn(SingletonComponent::class) abstract class RecordingModule { @Binds - abstract fun bindWearRecordingControlUseCase( - implementation: WearMeasurementControlUseCase, - ): WearRecordingControlUseCase + abstract fun bindWatchRecordingControlUseCase( + implementation: DefaultWatchRecordingControlUseCase, + ): WatchRecordingControlUseCase @Binds abstract fun bindWearRecordingRequirementsUseCase( diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementControlUseCase.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingControlUseCase.kt similarity index 74% rename from wear/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementControlUseCase.kt rename to wear/src/main/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingControlUseCase.kt index 3238f78..83ab4cf 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementControlUseCase.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingControlUseCase.kt @@ -1,4 +1,4 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.content.Context import android.content.Intent @@ -8,24 +8,24 @@ import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.appResult import com.tomasrepcik.sensorbox.core.preferences.AppPreferences -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementIntentFactory -import com.tomasrepcik.sensorbox.sensorservices.intent.MeasurementLaunchRequest -import com.tomasrepcik.sensorbox.sensorservices.services.MeasurementService +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingIntentFactory +import com.tomasrepcik.sensorbox.sensorservices.intent.RecordingRequest +import com.tomasrepcik.sensorbox.sensorservices.services.RecordingService import com.tomasrepcik.sensorbox.wearoslib.protocol.WearRecordingRequest import com.tomasrepcik.sensorbox.wearoslib.protocol.WearStopReason import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -interface WearRecordingControlUseCase { +interface WatchRecordingControlUseCase { fun start(sessionId: String, request: WearRecordingRequest, preferences: AppPreferences): AppResult fun stop(sessionId: String, reason: WearStopReason): AppResult } -class WearMeasurementControlUseCase @Inject constructor( +class DefaultWatchRecordingControlUseCase @Inject constructor( @ApplicationContext private val context: Context, - private val intentFactory: MeasurementIntentFactory, -) : WearRecordingControlUseCase { + private val intentFactory: RecordingIntentFactory, +) : WatchRecordingControlUseCase { override fun start( sessionId: String, request: WearRecordingRequest, @@ -46,15 +46,15 @@ class WearMeasurementControlUseCase @Inject constructor( sessionId: String = java.util.UUID.randomUUID().toString(), folderName: String = intentFactory.newFolderName(), durationMillis: Long = 0L, - ): AppResult = appResult(AppErrorCode.MEASUREMENT, "Request Wear measurement start") { - val request = MeasurementLaunchRequest( + ): AppResult = appResult(AppErrorCode.RECORDING, "Request watch recording start") { + val request = RecordingRequest( sessionId = sessionId, folderName = folderName, useInternalStorage = true, sensorIds = sensorIds, sensorSamplingPeriod = samplingPeriod(preferences.recording.sensorSamplingPeriod), includesGps = includesGps, - stopOnLowBattery = preferences.recording.restrictMeasurementOnLowBattery, + stopOnLowBattery = preferences.recording.stopRecordingOnLowBattery, useWakeLock = preferences.recording.useWakeLock, gpsIntervalSeconds = preferences.recording.gpsIntervalSeconds, gpsMinDistanceMeters = preferences.recording.gpsMinDistanceMeters, @@ -63,9 +63,9 @@ class WearMeasurementControlUseCase @Inject constructor( ContextCompat.startForegroundService(context, intentFactory.create(request)) } - fun stop(): AppResult = appResult(AppErrorCode.MEASUREMENT, "Request Wear measurement stop") { - val intent = Intent(context, MeasurementService::class.java) - .setAction(MeasurementService.ACTION_STOP) + fun stop(): AppResult = appResult(AppErrorCode.RECORDING, "Request watch recording stop") { + val intent = Intent(context, RecordingService::class.java) + .setAction(RecordingService.ACTION_STOP_RECORDING) context.startService(intent) } diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementPermissionUseCase.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingPermissionUseCase.kt similarity index 59% rename from wear/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementPermissionUseCase.kt rename to wear/src/main/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingPermissionUseCase.kt index 286a2ab..fa12ec1 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementPermissionUseCase.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingPermissionUseCase.kt @@ -1,4 +1,4 @@ -package com.tomasrepcik.sensorbox.domain.measurement +package com.tomasrepcik.sensorbox.domain.recording import android.Manifest import android.content.Context @@ -7,14 +7,14 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class WearMeasurementPermissionUseCase @Inject constructor(@ApplicationContext private val context: Context) { +class WatchRecordingPermissionUseCase @Inject constructor(@ApplicationContext private val context: Context) { operator fun invoke(includesGps: Boolean): Set = - requiredWearMeasurementPermissions(includesGps).filterNot(::isGranted).toSet() + requiredWatchRecordingPermissions(includesGps).filterNot(::isGranted).toSet() private fun isGranted(permission: String): Boolean = ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED } -internal fun requiredWearMeasurementPermissions(includesGps: Boolean): Set = buildSet { +internal fun requiredWatchRecordingPermissions(includesGps: Boolean): Set = buildSet { if (includesGps) add(Manifest.permission.ACCESS_FINE_LOCATION) } diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/GetWearSensorsUseCase.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/GetWatchSensorsUseCase.kt similarity index 83% rename from wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/GetWearSensorsUseCase.kt rename to wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/GetWatchSensorsUseCase.kt index e31633a..fa0657b 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/GetWearSensorsUseCase.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/GetWatchSensorsUseCase.kt @@ -6,17 +6,17 @@ import android.hardware.SensorManager import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject -class GetWearSensorsUseCase @Inject constructor(@ApplicationContext context: Context) { +class GetWatchSensorsUseCase @Inject constructor(@ApplicationContext context: Context) { private val sensorManager = context.getSystemService(SensorManager::class.java) - operator fun invoke(): List = sensorManager + operator fun invoke(): List = sensorManager .getSensorList(Sensor.TYPE_ALL) .filter { it.type in WEAR_SENSOR_TYPES } .distinctBy(Sensor::getType) .map { sensor -> sensor.toDescriptor() } - .sortedBy(WearSensorDescriptor::name) + .sortedBy(WatchSensorDescriptor::name) - private fun Sensor.toDescriptor() = WearSensorDescriptor( + private fun Sensor.toDescriptor() = WatchSensorDescriptor( type = type, name = name, vendor = vendor, diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorDescriptor.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorDescriptor.kt similarity index 89% rename from wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorDescriptor.kt rename to wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorDescriptor.kt index 8122c25..1055a61 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorDescriptor.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorDescriptor.kt @@ -2,7 +2,7 @@ package com.tomasrepcik.sensorbox.domain.sensors import com.tomasrepcik.sensorbox.wearoslib.protocol.WearSensorInfo -data class WearSensorDescriptor( +data class WatchSensorDescriptor( val type: Int, val name: String, val vendor: String, @@ -17,7 +17,7 @@ data class WearSensorDescriptor( val isWakeUpSensor: Boolean, ) -fun WearSensorDescriptor.toWearSensorInfo() = WearSensorInfo( +fun WatchSensorDescriptor.toWatchSensorInfo() = WearSensorInfo( type = type, name = name, vendor = vendor, diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sync/SyncWearMeasurementsUseCase.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sync/SyncWatchMeasurementsUseCase.kt similarity index 90% rename from wear/src/main/java/com/tomasrepcik/sensorbox/domain/sync/SyncWearMeasurementsUseCase.kt rename to wear/src/main/java/com/tomasrepcik/sensorbox/domain/sync/SyncWatchMeasurementsUseCase.kt index bbc6068..7497642 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sync/SyncWearMeasurementsUseCase.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/domain/sync/SyncWatchMeasurementsUseCase.kt @@ -16,7 +16,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File import javax.inject.Inject -class SyncWearMeasurementsUseCase @Inject constructor( +class SyncWatchMeasurementsUseCase @Inject constructor( @ApplicationContext private val context: Context, private val connectionRepository: WearConnectionRepository, private val transferClient: WearFileTransferClient, @@ -27,7 +27,7 @@ class SyncWearMeasurementsUseCase @Inject constructor( if (node == null) { return@suspendFlatMap AppResult.failure(AppError(AppErrorCode.CONNECTIVITY, "Find connected phone")) } - appResult(AppErrorCode.STORAGE, "List Wear measurements", ::measurementFiles).suspendFlatMap { files -> + appResult(AppErrorCode.STORAGE, "List watch measurements", ::measurementFiles).suspendFlatMap { files -> var transferResult: AppResult = AppResult.success(Unit) for (file in files) { if (transferResult.isFailure) break @@ -35,11 +35,11 @@ class SyncWearMeasurementsUseCase @Inject constructor( } transferResult.map { files.size } } - }.withAppError(AppErrorCode.CONNECTIVITY, "Sync Wear measurements") + }.withAppError(AppErrorCode.CONNECTIVITY, "Sync watch measurements") private suspend fun sendFile(nodeId: String, file: File): AppResult { val measurementName = file.parentFile?.name - ?: return AppResult.failure(AppError(AppErrorCode.STORAGE, "Read Wear measurement folder")) + ?: return AppResult.failure(AppError(AppErrorCode.STORAGE, "Read watch measurement folder")) return transferClient.send( nodeId = nodeId, metadata = WearFileMetadata(measurementName, file.name), diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/WearComponents.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/WearComponents.kt index dc96e23..ba1aad8 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/WearComponents.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/WearComponents.kt @@ -191,7 +191,7 @@ internal fun TransformingLazyColumnItemScope.WearSwitchRow( } @Composable -internal fun TransformingLazyColumnItemScope.WearPrimaryAction( +internal fun TransformingLazyColumnItemScope.WearPrimaryButton( label: String, transformation: TransformationSpec, enabled: Boolean = true, @@ -208,7 +208,7 @@ internal fun TransformingLazyColumnItemScope.WearPrimaryAction( } @Composable -internal fun TransformingLazyColumnItemScope.WearSecondaryAction( +internal fun TransformingLazyColumnItemScope.WearSecondaryButton( label: String, transformation: TransformationSpec, onClick: () -> Unit, @@ -230,7 +230,7 @@ internal fun TransformingLazyColumnItemScope.WearSecondaryAction( } @Composable -internal fun WearPrimaryEdgeAction(label: String, enabled: Boolean = true, onClick: () -> Unit) { +internal fun WearPrimaryEdgeButton(label: String, enabled: Boolean = true, onClick: () -> Unit) { EdgeButton( onClick = onClick, enabled = enabled, diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardContract.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardContract.kt index 156a93f..4d8cb4b 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardContract.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardContract.kt @@ -1,15 +1,15 @@ package com.tomasrepcik.sensorbox.presentation.dashboard import com.tomasrepcik.sensorbox.core.preferences.AppPreferences -import com.tomasrepcik.sensorbox.domain.sensors.WearSensorDescriptor +import com.tomasrepcik.sensorbox.domain.sensors.WatchSensorDescriptor import com.tomasrepcik.sensorbox.presentation.menu.WearMenuDestination -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState enum class WearRoute { MENU, RECORD, LIVE, SETTINGS, ACTIVE } data class WearDashboardState( val route: WearRoute = WearRoute.MENU, - val sensors: List = emptyList(), + val sensors: List = emptyList(), val selectedSensorIds: Set = emptySet(), val includesGps: Boolean = false, val liveSensorType: Int? = null, @@ -33,9 +33,9 @@ sealed interface WearDashboardIntent { data object Back : WearDashboardIntent data class ToggleSensor(val sensorType: Int) : WearDashboardIntent data object ToggleGps : WearDashboardIntent - data object StartMeasurement : WearDashboardIntent + data object StartRecording : WearDashboardIntent data class PermissionsResolved(val granted: Boolean) : WearDashboardIntent - data object StopMeasurement : WearDashboardIntent + data object StopRecording : WearDashboardIntent data class ObserveSensor(val sensorType: Int) : WearDashboardIntent data class SetSamplingPeriod(val index: Int) : WearDashboardIntent data object ToggleBatteryRestriction : WearDashboardIntent @@ -60,10 +60,10 @@ object WearDashboardReducer { else -> state } - fun measurementSessionChanged(state: WearDashboardState, session: MeasurementSessionState) = when { - session is MeasurementSessionState.Running -> state.copy(route = WearRoute.ACTIVE) + fun recordingSessionChanged(state: WearDashboardState, session: RecordingSessionState) = when { + session is RecordingSessionState.Running -> state.copy(route = WearRoute.ACTIVE) - session is MeasurementSessionState.Idle && state.route == WearRoute.ACTIVE -> + session is RecordingSessionState.Idle && state.route == WearRoute.ACTIVE -> state.copy(route = WearRoute.MENU) else -> state diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardScreen.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardScreen.kt index 568674a..66db328 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardScreen.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardScreen.kt @@ -31,15 +31,15 @@ import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLa import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.tomasrepcik.sensorbox.R import com.tomasrepcik.sensorbox.core.format.ValueFormats -import com.tomasrepcik.sensorbox.domain.sensors.WearSensorDescriptor +import com.tomasrepcik.sensorbox.domain.sensors.WatchSensorDescriptor import com.tomasrepcik.sensorbox.presentation.WearCheckboxRow import com.tomasrepcik.sensorbox.presentation.WearListScreen import com.tomasrepcik.sensorbox.presentation.WearNavigationRow import com.tomasrepcik.sensorbox.presentation.WearPageTitle -import com.tomasrepcik.sensorbox.presentation.WearPrimaryAction -import com.tomasrepcik.sensorbox.presentation.WearPrimaryEdgeAction +import com.tomasrepcik.sensorbox.presentation.WearPrimaryButton +import com.tomasrepcik.sensorbox.presentation.WearPrimaryEdgeButton import com.tomasrepcik.sensorbox.presentation.WearRadioRow -import com.tomasrepcik.sensorbox.presentation.WearSecondaryAction +import com.tomasrepcik.sensorbox.presentation.WearSecondaryButton import com.tomasrepcik.sensorbox.presentation.WearSectionTitle import com.tomasrepcik.sensorbox.presentation.WearSwitchRow import com.tomasrepcik.sensorbox.presentation.menu.WearMenuScreen @@ -65,10 +65,10 @@ private fun WearRecordScreen(state: WearDashboardState, accept: (WearDashboardIn val selectedCount = state.selectedSensorIds.size + if (state.includesGps) 1 else 0 WearListScreen( edgeButton = { - WearPrimaryEdgeAction( + WearPrimaryEdgeButton( label = pluralStringResource(R.plurals.start_source_count, selectedCount, selectedCount), enabled = selectedCount > 0, - onClick = { accept(WearDashboardIntent.StartMeasurement) }, + onClick = { accept(WearDashboardIntent.StartRecording) }, ) }, ) { transformation -> @@ -97,7 +97,7 @@ private fun WearRecordScreen(state: WearDashboardState, accept: (WearDashboardIn state.message?.let { message -> item { WearSectionTitle(wearMessageText(message), transformation) } } - item { WearBackAction(transformation, accept) } + item { WearBackButton(transformation, accept) } } } @@ -150,7 +150,7 @@ private fun WearLiveScreen( } @Composable -private fun WearSensorPicker(sensors: List, accept: (WearDashboardIntent) -> Unit) { +private fun WearSensorPicker(sensors: List, accept: (WearDashboardIntent) -> Unit) { WearListScreen { transformation -> item { WearPageTitle(stringResource(R.string.live_values), transformation) } sensors.forEach { sensor -> @@ -163,7 +163,7 @@ private fun WearSensorPicker(sensors: List, accept: (WearD ) { accept(WearDashboardIntent.ObserveSensor(sensor.type)) } } } - item { WearBackAction(transformation, accept) } + item { WearBackButton(transformation, accept) } } } @@ -174,7 +174,7 @@ private fun WearSettingsScreen(state: WearDashboardState, accept: (WearDashboard preferenceItems(state, transformation, accept) item { WearSectionTitle(stringResource(R.string.transfer), transformation) } item { - WearPrimaryAction( + WearPrimaryButton( label = stringResource(if (state.isSyncing) R.string.syncing else R.string.sync_recordings), transformation = transformation, enabled = !state.isSyncing, @@ -190,7 +190,7 @@ private fun WearSettingsScreen(state: WearDashboardState, accept: (WearDashboard state.message?.let { message -> item { WearSectionTitle(wearMessageText(message), transformation) } } - item { WearBackAction(transformation, accept) } + item { WearBackButton(transformation, accept) } } } @@ -218,7 +218,7 @@ private fun TransformingLazyColumnScope.preferenceItems( item { WearSwitchRow( label = stringResource(R.string.stop_on_low_battery), - checked = state.preferences.recording.restrictMeasurementOnLowBattery, + checked = state.preferences.recording.stopRecordingOnLowBattery, transformation = transformation, onCheckedChange = { accept(WearDashboardIntent.ToggleBatteryRestriction) }, ) @@ -274,7 +274,7 @@ private fun WearActiveScreen(state: WearDashboardState, accept: (WearDashboardIn ) Spacer(Modifier.height(22.dp)) Button( - onClick = { accept(WearDashboardIntent.StopMeasurement) }, + onClick = { accept(WearDashboardIntent.StopRecording) }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error, @@ -289,11 +289,11 @@ private fun WearActiveScreen(state: WearDashboardState, accept: (WearDashboardIn } @Composable -private fun TransformingLazyColumnItemScope.WearBackAction( +private fun TransformingLazyColumnItemScope.WearBackButton( transformation: TransformationSpec, accept: (WearDashboardIntent) -> Unit, ) { - WearSecondaryAction(stringResource(R.string.back), transformation) { + WearSecondaryButton(stringResource(R.string.back), transformation) { accept(WearDashboardIntent.Back) } } diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardViewModel.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardViewModel.kt index dc67329..1022a09 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardViewModel.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardViewModel.kt @@ -9,13 +9,13 @@ import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesIntent import com.tomasrepcik.sensorbox.core.preferences.AppPreferencesRepository -import com.tomasrepcik.sensorbox.domain.measurement.WearMeasurementControlUseCase -import com.tomasrepcik.sensorbox.domain.measurement.WearMeasurementPermissionUseCase -import com.tomasrepcik.sensorbox.domain.sensors.GetWearSensorsUseCase +import com.tomasrepcik.sensorbox.domain.recording.DefaultWatchRecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.WatchRecordingPermissionUseCase +import com.tomasrepcik.sensorbox.domain.sensors.GetWatchSensorsUseCase import com.tomasrepcik.sensorbox.domain.sensors.ObserveSensorValuesUseCase -import com.tomasrepcik.sensorbox.domain.sync.SyncWearMeasurementsUseCase +import com.tomasrepcik.sensorbox.domain.sync.SyncWatchMeasurementsUseCase import com.tomasrepcik.sensorbox.presentation.menu.WearMenuDestination -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionStore +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionStore import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -28,13 +28,13 @@ import javax.inject.Inject @HiltViewModel class WearDashboardViewModel @Inject constructor( - getSensors: GetWearSensorsUseCase, + getSensors: GetWatchSensorsUseCase, private val observeSensorValues: ObserveSensorValuesUseCase, - private val permissionUseCase: WearMeasurementPermissionUseCase, - private val measurementControl: WearMeasurementControlUseCase, + private val permissionUseCase: WatchRecordingPermissionUseCase, + private val recordingControl: DefaultWatchRecordingControlUseCase, private val preferencesRepository: AppPreferencesRepository, - private val sessionStore: MeasurementSessionStore, - private val syncMeasurements: SyncWearMeasurementsUseCase, + private val sessionStore: RecordingSessionStore, + private val syncMeasurements: SyncWatchMeasurementsUseCase, ) : ViewModel() { private val mutableState = MutableStateFlow(WearDashboardState(sensors = getSensors())) private val mutableEffects = Channel(Channel.BUFFERED) @@ -54,9 +54,9 @@ class WearDashboardViewModel @Inject constructor( mutableState.value = WearDashboardReducer.reduce(mutableState.value, intent) when (intent) { is WearDashboardIntent.Open -> handleDestination(intent.destination) - WearDashboardIntent.StartMeasurement -> requestMeasurementStart() + WearDashboardIntent.StartRecording -> requestRecordingStart() is WearDashboardIntent.PermissionsResolved -> handlePermissionResult(intent.granted) - WearDashboardIntent.StopMeasurement -> measurementControl.stop().showFailure() + WearDashboardIntent.StopRecording -> recordingControl.stop().showFailure() is WearDashboardIntent.ObserveSensor -> observeLiveSensor(intent.sensorType) is WearDashboardIntent.SetSamplingPeriod -> setSamplingPeriod(intent.index) WearDashboardIntent.ToggleBatteryRestriction -> toggleBatteryRestriction() @@ -80,14 +80,14 @@ class WearDashboardViewModel @Inject constructor( mutableEffects.trySend(WearDashboardEffect.OpenUrl(destination)) } - private fun requestMeasurementStart() { + private fun requestRecordingStart() { val state = mutableState.value if (state.selectedSensorIds.isEmpty() && !state.includesGps) { mutableState.value = state.copy(message = WearDashboardMessage.PickSource) return } val missing = permissionUseCase(state.includesGps) - if (missing.isEmpty()) startMeasurement() else requestPermissions(missing) + if (missing.isEmpty()) startRecording() else requestPermissions(missing) } private fun requestPermissions(permissions: Set) { @@ -96,14 +96,14 @@ class WearDashboardViewModel @Inject constructor( } private fun handlePermissionResult(granted: Boolean) { - if (granted && pendingStart) startMeasurement() + if (granted && pendingStart) startRecording() if (!granted) mutableState.value = mutableState.value.copy(message = WearDashboardMessage.PermissionRequired) pendingStart = false } - private fun startMeasurement() { + private fun startRecording() { val state = mutableState.value - measurementControl.start(state.selectedSensorIds, state.includesGps, state.preferences).showFailure() + recordingControl.start(state.selectedSensorIds, state.includesGps, state.preferences).showFailure() } private fun observeLiveSensor(sensorType: Int) { @@ -111,7 +111,7 @@ class WearDashboardViewModel @Inject constructor( sensorJob = viewModelScope.launch { observeSensorValues(sensorType) .catch { error -> - AppError.from(AppErrorCode.MEASUREMENT, "Observe live sensor", error) + AppError.from(AppErrorCode.RECORDING, "Observe live sensor", error) showSensorError() } .collect(::publishSensorValue) @@ -142,7 +142,7 @@ class WearDashboardViewModel @Inject constructor( private fun observeSession() = viewModelScope.launch { sessionStore.state.collect { session -> - mutableState.value = WearDashboardReducer.measurementSessionChanged(mutableState.value, session) + mutableState.value = WearDashboardReducer.recordingSessionChanged(mutableState.value, session) } } @@ -155,8 +155,8 @@ class WearDashboardViewModel @Inject constructor( ) private fun toggleBatteryRestriction() = updatePreference( - AppPreferencesIntent.SetLowBatteryRestriction( - !mutableState.value.preferences.recording.restrictMeasurementOnLowBattery, + AppPreferencesIntent.SetStopOnLowBattery( + !mutableState.value.preferences.recording.stopRecordingOnLowBattery, ), ) diff --git a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/phonelaunch/PhoneLaunchScreen.kt b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/phonelaunch/PhoneLaunchScreen.kt index 6ee10d3..a6f44c2 100644 --- a/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/phonelaunch/PhoneLaunchScreen.kt +++ b/wear/src/main/java/com/tomasrepcik/sensorbox/presentation/phonelaunch/PhoneLaunchScreen.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.res.stringResource import com.tomasrepcik.sensorbox.R import com.tomasrepcik.sensorbox.presentation.WearListScreen import com.tomasrepcik.sensorbox.presentation.WearPageTitle -import com.tomasrepcik.sensorbox.presentation.WearPrimaryAction +import com.tomasrepcik.sensorbox.presentation.WearPrimaryButton import com.tomasrepcik.sensorbox.presentation.WearSectionTitle @Composable @@ -14,7 +14,7 @@ fun PhoneLaunchScreen(state: PhoneLaunchState, onLaunchPhone: () -> Unit) { item { WearPageTitle(stringResource(R.string.activity_info_phone), transformation) } item { WearSectionTitle(stringResource(statusText(state.status)), transformation) } item { - WearPrimaryAction( + WearPrimaryButton( label = stringResource(R.string.open_phone), transformation = transformation, onClick = onLaunchPhone, diff --git a/wear/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/dashboard/ReadmeWearScreenshotPreviews.kt b/wear/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/dashboard/ReadmeWearScreenshotPreviews.kt index a519c5b..8038dac 100644 --- a/wear/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/dashboard/ReadmeWearScreenshotPreviews.kt +++ b/wear/src/screenshotTest/kotlin/com/tomasrepcik/sensorbox/presentation/dashboard/ReadmeWearScreenshotPreviews.kt @@ -19,7 +19,7 @@ import com.patrykandpatrick.vico.compose.cartesian.data.CartesianChartModelProdu import com.tomasrepcik.sensorbox.core.preferences.AppPreferences import com.tomasrepcik.sensorbox.core.preferences.DisplayPreferences import com.tomasrepcik.sensorbox.core.preferences.RecordingPreferences -import com.tomasrepcik.sensorbox.domain.sensors.WearSensorDescriptor +import com.tomasrepcik.sensorbox.domain.sensors.WatchSensorDescriptor import com.tomasrepcik.sensorbox.ui.theme.WearSensorBoxTheme @PreviewTest @@ -99,7 +99,7 @@ private fun readmeWearSensors() = listOf( readmeWearSensor(2, "Goldfish 3-axis Magnetic field sensor"), ) -private fun readmeWearSensor(type: Int, name: String) = WearSensorDescriptor( +private fun readmeWearSensor(type: Int, name: String) = WatchSensorDescriptor( type = type, name = name, vendor = "Android", diff --git a/wear/src/test/java/com/tomasrepcik/sensorbox/communication/WearCommandHandlerTest.kt b/wear/src/test/java/com/tomasrepcik/sensorbox/communication/WearCommandHandlerTest.kt index 0524e03..2a8565c 100644 --- a/wear/src/test/java/com/tomasrepcik/sensorbox/communication/WearCommandHandlerTest.kt +++ b/wear/src/test/java/com/tomasrepcik/sensorbox/communication/WearCommandHandlerTest.kt @@ -5,7 +5,7 @@ import com.tomasrepcik.sensorbox.core.error.AppErrorCode import com.tomasrepcik.sensorbox.core.error.AppResult import com.tomasrepcik.sensorbox.core.error.DiagnosticLogger import com.tomasrepcik.sensorbox.core.preferences.AppPreferences -import com.tomasrepcik.sensorbox.domain.measurement.WearRecordingControlUseCase +import com.tomasrepcik.sensorbox.domain.recording.WatchRecordingControlUseCase import com.tomasrepcik.sensorbox.wearoslib.WearOsConstants.WEAR_MESSAGE_PATH import com.tomasrepcik.sensorbox.wearoslib.connectivity.SendWearMessageUseCase import com.tomasrepcik.sensorbox.wearoslib.connectivity.WearConnection @@ -69,7 +69,7 @@ class WearCommandHandlerTest { fixture.requirements.validationResult = AppResult.failure( AppError( code = AppErrorCode.PERMISSION, - operation = "Validate Wear recording permissions", + operation = "Validate watch recording permissions", diagnosticMessage = "Wear OS is missing required recording permissions", context = mapOf("missingPermissions" to "android.permission.ACCESS_FINE_LOCATION"), ), @@ -82,7 +82,7 @@ class WearCommandHandlerTest { val result = fixture.repository.commands.single() as WearCommand.RecordingResult assertEquals(WearRecordingOutcome.FAILED, result.outcome) assertEquals(AppErrorCode.PERMISSION, result.errorCode) - assertEquals("Validate Wear recording permissions", result.errorOperation) + assertEquals("Validate watch recording permissions", result.errorOperation) assertEquals( "android.permission.ACCESS_FINE_LOCATION", result.errorContext["missingPermissions"], @@ -139,7 +139,7 @@ class WearCommandHandlerTest { } } -private class FakeWearRecordingControl : WearRecordingControlUseCase { +private class FakeWearRecordingControl : WatchRecordingControlUseCase { var startCalls = 0 var stopCalls = 0 diff --git a/wear/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementPermissionUseCaseTest.kt b/wear/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementPermissionUseCaseTest.kt deleted file mode 100644 index da50589..0000000 --- a/wear/src/test/java/com/tomasrepcik/sensorbox/domain/measurement/WearMeasurementPermissionUseCaseTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tomasrepcik.sensorbox.domain.measurement - -import android.Manifest -import org.junit.Assert.assertEquals -import org.junit.Test - -class WearMeasurementPermissionUseCaseTest { - @Test - fun `Given Wear GPS When permissions are derived Then only location is required`() { - assertEquals( - setOf(Manifest.permission.ACCESS_FINE_LOCATION), - requiredWearMeasurementPermissions(includesGps = true), - ) - } - - @Test - fun `Given Wear sensors without GPS When permissions are derived Then none are required`() { - assertEquals( - emptySet(), - requiredWearMeasurementPermissions(includesGps = false), - ) - } -} diff --git a/wear/src/test/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingPermissionUseCaseTest.kt b/wear/src/test/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingPermissionUseCaseTest.kt new file mode 100644 index 0000000..726b64a --- /dev/null +++ b/wear/src/test/java/com/tomasrepcik/sensorbox/domain/recording/WatchRecordingPermissionUseCaseTest.kt @@ -0,0 +1,23 @@ +package com.tomasrepcik.sensorbox.domain.recording + +import android.Manifest +import org.junit.Assert.assertEquals +import org.junit.Test + +class WatchRecordingPermissionUseCaseTest { + @Test + fun `Given watch GPS When permissions are derived Then only location is required`() { + assertEquals( + setOf(Manifest.permission.ACCESS_FINE_LOCATION), + requiredWatchRecordingPermissions(includesGps = true), + ) + } + + @Test + fun `Given watch sensors without GPS When permissions are derived Then none are required`() { + assertEquals( + emptySet(), + requiredWatchRecordingPermissions(includesGps = false), + ) + } +} diff --git a/wear/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorTypesTest.kt b/wear/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorTypesTest.kt similarity index 62% rename from wear/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorTypesTest.kt rename to wear/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorTypesTest.kt index 98c23d1..5d77e36 100644 --- a/wear/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WearSensorTypesTest.kt +++ b/wear/src/test/java/com/tomasrepcik/sensorbox/domain/sensors/WatchSensorTypesTest.kt @@ -4,9 +4,9 @@ import android.hardware.Sensor import org.junit.Assert.assertFalse import org.junit.Test -class WearSensorTypesTest { +class WatchSensorTypesTest { @Test - fun `Given Wear sensor discovery When supported types are inspected Then heart rate is absent`() { + fun `Given watch sensor discovery When supported types are inspected Then heart rate is absent`() { assertFalse(Sensor.TYPE_HEART_RATE in WEAR_SENSOR_TYPES) } } diff --git a/wear/src/test/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardReducerTest.kt b/wear/src/test/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardReducerTest.kt index c9ef21d..2fcd87e 100644 --- a/wear/src/test/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardReducerTest.kt +++ b/wear/src/test/java/com/tomasrepcik/sensorbox/presentation/dashboard/WearDashboardReducerTest.kt @@ -2,7 +2,7 @@ package com.tomasrepcik.sensorbox.presentation.dashboard import com.tomasrepcik.sensorbox.core.testing.AppPreferencesFixtures import com.tomasrepcik.sensorbox.presentation.menu.WearMenuDestination -import com.tomasrepcik.sensorbox.sensorservices.session.MeasurementSessionState +import com.tomasrepcik.sensorbox.sensorservices.session.RecordingSessionState import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -54,12 +54,12 @@ class WearDashboardReducerTest { @Test fun `Given recording screen When recording finishes Then menu with sync is restored`() { - val active = WearDashboardReducer.measurementSessionChanged( + val active = WearDashboardReducer.recordingSessionChanged( WearDashboardState(route = WearRoute.RECORD), - MeasurementSessionState.Running("session", "fixture", 1L, listOf(1), false), + RecordingSessionState.Running("session", "fixture", 1L, listOf(1), false), ) - val finished = WearDashboardReducer.measurementSessionChanged(active, MeasurementSessionState.Idle) + val finished = WearDashboardReducer.recordingSessionChanged(active, RecordingSessionState.Idle) assertEquals(WearRoute.ACTIVE, active.route) assertEquals(WearRoute.MENU, finished.route) From b175e2bd3a8f7c3a0795d737c772cf1809ed26b6 Mon Sep 17 00:00:00 2001 From: Foxpace Date: Fri, 28 Aug 2026 23:58:50 +0200 Subject: [PATCH 2/2] docs: remove duplicate context glossary --- CONTEXT.md | 65 ------------------------------------------------------ 1 file changed, 65 deletions(-) delete mode 100644 CONTEXT.md diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 18c668b..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,65 +0,0 @@ -# Recording archive - -The recording archive is the selected phone directory containing phone measurements and synced watch measurements. - -## Language - -**Measurement** - -One device's stored output for a recording session. A phone and watch can each create a measurement for the same paired recording. A metadata-only measurement is valid. - -_Avoid:_ Recording session, recording file, sensor measurement - -**Measurement file** - -One data stream inside a measurement, such as accelerometer samples or GPS coordinates. - -_Avoid:_ Measurement - -**Measurement metadata** - -Session-level facts stored with a measurement, including its start time, notes, alarms, duration, and sensor ranges. - -_Avoid:_ Sensor metadata - -**Recording session** - -One start-to-stop recording operation. It can run on the phone alone or on both phone and watch. - -_Avoid:_ Measurement session - -**Recording setup** - -The editable choices made before a recording starts. - -_Avoid:_ Measurement setup, recording configuration - -**Recording request** - -The immutable command created from the recording setup when recording starts. - -_Avoid:_ Recording plan, measurement request, launch configuration - -**Recording source** - -An independently managed data producer, such as sensors or GPS. One recording source can create several measurement files. - -_Avoid:_ Recording device - -**Paired recording** - -One recording session coordinated across the phone and watch. - -_Avoid:_ watch recording - -**Sample** - -One timestamped observation, normally stored as one row in a measurement file. - -_Avoid:_ Sensor measurement - -**Sync** - -Copying watch measurements to the phone recording archive without merging them or removing the watch originals. - -_Avoid:_ Merge, move