feat(train): setting up and keeping the Polar Loop (ADR 0030, D34–D36)

On Android, the Loop through Polar's BLE SDK 8.3.0 (from JitPack, asked
for Polar's group alone, its files pinned by SHA-256 in Gradle's
verification metadata): LoopBridge.kt over app.halkyon.train/loop, the
first-time use's figures mapped and checked in LoopSetup.kt, tested on
the JVM. PolarLoop per person remembers the band in loop.json, connects
while the app is open, sets its clock at each connection, keeps what it
tells (battery, charging, firmware), offers a changed profile and sends
it only when asked (D35: first-time use writes a new user identifier
each time), and lets the band go by a factory reset after asking. The
setup sheet finds Loops nearest first and asks only for what the
profile lacks; Polar's defaults (60, 220 less the age, a VO2max from
Polar's fitness tables) go to the band alone.

Polar's SDK is not free software (D36, correcting D32): the app's AGPL
gains a section 7 permission for linking it, LICENSE.md names it, and
its licence and bundled notices join the licence list on Android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 18:58:02 +01:00
commit cd72cf6ccc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
40 changed files with 3485 additions and 99 deletions

View file

@ -17,14 +17,17 @@ The repository's packages carry different licences, each package's full text in
| `apps/todo/halkyon_todo_flutter`, Todo's app | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` | | `apps/todo/halkyon_todo_flutter`, Todo's app | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
| `apps/todo/halkyon_todo_client`, generated from Todo's models | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` | | `apps/todo/halkyon_todo_client`, generated from Todo's models | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
| `apps/todo/halkyon_todo_server` | Server Side Public License, version 1 (decided 2026-10-19, as Learn's) | `SSPL-1.0` | | `apps/todo/halkyon_todo_server` | Server Side Public License, version 1 (decided 2026-10-19, as Learn's) | `SSPL-1.0` |
| `apps/train/halkyon_train_flutter`, Train's app | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` | | `apps/train/halkyon_train_flutter`, Train's app, with an added permission for linking Polar's BLE SDK (`LICENSE-polar-exception.md`) | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later WITH LicenseRef-Polar-BLE-SDK-exception` |
| `apps/train/halkyon_train_client`, generated from Train's models | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` | | `apps/train/halkyon_train_client`, generated from Train's models | GNU Affero General Public License, version 3 or any later version | `AGPL-3.0-or-later` |
| `apps/train/halkyon_train_server` | Server Side Public License, version 1 (as Learn's and Todo's) | `SSPL-1.0` | | `apps/train/halkyon_train_server` | Server Side Public License, version 1 (as Learn's and Todo's) | `SSPL-1.0` |
The servers and the module build on Serverpod's core, which is under the SSPL, and so are The servers and the module build on Serverpod's core, which is under the SSPL, and so are
under it too. The apps and the shared Flutter packages link only permissively licensed under it too. The apps and the shared Flutter packages link only permissively licensed
packages (the apps' icons are Lucide's, under the ISC licence, whose notice each app registers), and the client packages are generated from our own model definitions, so all of packages (the apps' icons are Lucide's, under the ISC licence, whose notice each app registers), and the client packages are generated from our own model definitions, so all of
them are under the AGPL. The curator depends on neither a server nor Serverpod: Learn's them are under the AGPL. One exception: Train's Android build links Polar's BLE SDK for
the Polar Loop, under Polar's own, non-free licence (Train's ADR 0030, D36), which the
app's added permission allows; its notices are in the app's licence list, and an F-Droid
listing of Train is to declare `NonFreeDep`. The curator depends on neither a server nor Serverpod: Learn's
server reads the files it writes, never its code. server reads the files it writes, never its code.
Everything else in this repository (tooling, documentation, deployment files) is under Everything else in this repository (tooling, documentation, deployment files) is under

View file

@ -7,5 +7,6 @@ What holds for every app and package of this repository is in the [root AGENTS.m
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Train's frame (`trainFrame`: "Train" under "Halkyon", its icon, `train.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'train')`; `TrainDependencies` (`AppDependencies<TrainPerson>`) on one instance, `TrainPerson` per person: their own sync database in their folder (`sync.sqlite`, halkyon_app's `PersonFolders`, one person's at a time), halkyon_app's `SyncService` behind `AppCompatibility` (the module's basics' `minimumAppBuild`, against the build `package_info_plus` reads), their key (`TrainKeys`, `lib/src/core/privacy/`: one key per person, IDs `train1.<ms>.<nonce>`, the oldest-made writing, kept in `content_key` and opened only in Train's form, ADR 0030, D15) and their workouts (`WorkoutRepository`, `lib/src/feature/workout/`: halkyon_app's `PayloadEnvelope` seals a workout for its row and a series chunk for its row and its workout; payloads are versioned JSON, a series' columns delta-coded, gzipped before sealing; a deletion blanks every payload first); the gateways' tests run on a sync database opened without a server, the repository's on memory gateways and real libsodium; a person's app whose home lists their workouts (`WorkoutsCubit`, with `TrainPerson.deletions`, halkyon_app's `DelayedDeletions`) and opens each one's page (`WorkoutCubit`: `analyse` in `workout_analysis.dart` reads its series into the track's pieces, splits, elevation and charts, drawn by `workout_chart.dart`; the map with flutter_map on the instance's source, `MapSourceRepository` asking `map.source`, its tiles from `TrainPerson.mapTiles`, vector_map_tiles in the app and none in widget tests), and the account sheet: halkyon_app's `AccountTiles` (whose account on which instance, Switch account, Sign out, Delete account, which asks first) in Train's words, and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TrainDependencies.sourceCodeUrl`, and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Train has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Train's whole tree with halkyon_app's fakes (`test/app/train_app_test.dart`). - **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Train's frame (`trainFrame`: "Train" under "Halkyon", its icon, `train.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'train')`; `TrainDependencies` (`AppDependencies<TrainPerson>`) on one instance, `TrainPerson` per person: their own sync database in their folder (`sync.sqlite`, halkyon_app's `PersonFolders`, one person's at a time), halkyon_app's `SyncService` behind `AppCompatibility` (the module's basics' `minimumAppBuild`, against the build `package_info_plus` reads), their key (`TrainKeys`, `lib/src/core/privacy/`: one key per person, IDs `train1.<ms>.<nonce>`, the oldest-made writing, kept in `content_key` and opened only in Train's form, ADR 0030, D15) and their workouts (`WorkoutRepository`, `lib/src/feature/workout/`: halkyon_app's `PayloadEnvelope` seals a workout for its row and a series chunk for its row and its workout; payloads are versioned JSON, a series' columns delta-coded, gzipped before sealing; a deletion blanks every payload first); the gateways' tests run on a sync database opened without a server, the repository's on memory gateways and real libsodium; a person's app whose home lists their workouts (`WorkoutsCubit`, with `TrainPerson.deletions`, halkyon_app's `DelayedDeletions`) and opens each one's page (`WorkoutCubit`: `analyse` in `workout_analysis.dart` reads its series into the track's pieces, splits, elevation and charts, drawn by `workout_chart.dart`; the map with flutter_map on the instance's source, `MapSourceRepository` asking `map.source`, its tiles from `TrainPerson.mapTiles`, vector_map_tiles in the app and none in widget tests), and the account sheet: halkyon_app's `AccountTiles` (whose account on which instance, Switch account, Sign out, Delete account, which asks first) in Train's words, and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TrainDependencies.sourceCodeUrl`, and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Train has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Train's whole tree with halkyon_app's fakes (`test/app/train_app_test.dart`).
- **Recording** (ADR 0030, D13, D16, D18 to D21, D26 to D28) is Android's alone for now (iOS waits for a Mac, D25) (`TrainPerson.recording` null elsewhere): `RecorderService` (Kotlin, `android/app/src/main/kotlin/app/halkyon/train/`), a foreground service of the types `location` and `health` started from the open app, takes fixes from LocationManager's GPS (no Play services), steps from the step detector the heart rate from the strap and a row from the rower the person chose (ADR 0030, D26 to D28: `NotifyLink.kt` on Android's own Bluetooth API, no package, `HeartRateMeasurement.kt` parsing 0x2A37 and `RowerData.kt` FTMS's Rower Data 0x2AD1; `DeviceScanner.kt` finds them for the sheets over `…/recorder/straps` and `…/recorder/rowers`; each remembered in the person's `strap.json` and `rower.json`, `DeviceStore`), decides auto-pause (`AutoPause.kt`) and appends each sample as a line to the person's `recording.txt` (format in `recording_file.dart`), flushed every 5 s; `MainActivity` asks the permissions (never background location) and bridges `app.halkyon.train/recorder` (methods) and `…/recorder/lines` (the file so far, then each line). The app (`lib/src/feature/recording/`) follows those lines with `RecordingBloc`, works out what the screen and the notification show (`liveOf`, the saved totals' functions), and saves a recording as a workout and its series (`recordedWorkout`) or throws it away; one cut off, or stopped and not saved, is found as the person's app opens. Tests: the Dart side with `test/support/fake_recorder_platform.dart`; the Kotlin auto-pause and the strap's and the rower's parsers on the JVM, `JAVA_HOME=<Android Studio's jbr> ./gradlew :app:testDebugUnitTest` in `android/` after a first `flutter build apk` (which writes `gradlew`), not part of `tool/check.dart`, as CI's runner has no Android SDK. - **Recording** (ADR 0030, D13, D16, D18 to D21, D26 to D28) is Android's alone for now (iOS waits for a Mac, D25) (`TrainPerson.recording` null elsewhere): `RecorderService` (Kotlin, `android/app/src/main/kotlin/app/halkyon/train/`), a foreground service of the types `location` and `health` started from the open app, takes fixes from LocationManager's GPS (no Play services), steps from the step detector the heart rate from the strap and a row from the rower the person chose (ADR 0030, D26 to D28: `NotifyLink.kt` on Android's own Bluetooth API, no package, `HeartRateMeasurement.kt` parsing 0x2A37 and `RowerData.kt` FTMS's Rower Data 0x2AD1; `DeviceScanner.kt` finds them for the sheets over `…/recorder/straps` and `…/recorder/rowers`; each remembered in the person's `strap.json` and `rower.json`, `DeviceStore`), decides auto-pause (`AutoPause.kt`) and appends each sample as a line to the person's `recording.txt` (format in `recording_file.dart`), flushed every 5 s; `MainActivity` asks the permissions (never background location) and bridges `app.halkyon.train/recorder` (methods) and `…/recorder/lines` (the file so far, then each line). The app (`lib/src/feature/recording/`) follows those lines with `RecordingBloc`, works out what the screen and the notification show (`liveOf`, the saved totals' functions), and saves a recording as a workout and its series (`recordedWorkout`) or throws it away; one cut off, or stopped and not saved, is found as the person's app opens. Tests: the Dart side with `test/support/fake_recorder_platform.dart`; the Kotlin auto-pause and the strap's and the rower's parsers on the JVM, `JAVA_HOME=<Android Studio's jbr> ./gradlew :app:testDebugUnitTest` in `android/` after a first `flutter build apk` (which writes `gradlew`), not part of `tool/check.dart`, as CI's runner has no Android SDK.
- **Health Connect** (ADR 0030, D30, D31) is Android's (`TrainPerson.health` null elsewhere): `HealthConnectBridge.kt` on AndroidX's `connect-client` 1.1.0 over `app.halkyon.train/health` (its records' mapping in `HealthRecords.kt`, tested on the JVM; the rationale's links from Health Connect in the manifest, `…/health/rationale` to the app), `HealthPlatform` in Dart (`lib/src/feature/health/`), `HealthConnect` per person: with writing on, every workout written as `healthRecordsOf` makes it (client IDs from the workout's ID, a version counted up as it changes, a fingerprint deciding whether it did), written again or deleted as the workouts change and as the app opens, what was written kept in the person's `health_connect.json` (`HealthStore`, never synced); with reading on, the sessions of the apps the person turned on brought in as workouts through the changes token (`importedWorkout` in `health_import.dart`, the workout's `origin` in its payload; a double of one Train has brings at most its heart rate, `isDouble`; one deleted in Train is remembered so it stays out; a track only when asked for on its page, `bringInTrack`), as the app opens and comes to the front (`HealthConnectListener`, which opens the rationale too); the screen from the account sheet (`HealthScreen`, `HealthCubit`). Tests on `test/support/fake_health_platform.dart`. Train needs Android 8 (API 26) for it. - **Health Connect** (ADR 0030, D30, D31) is Android's (`TrainPerson.health` null elsewhere): `HealthConnectBridge.kt` on AndroidX's `connect-client` 1.1.0 over `app.halkyon.train/health` (its records' mapping in `HealthRecords.kt`, tested on the JVM; the rationale's links from Health Connect in the manifest, `…/health/rationale` to the app), `HealthPlatform` in Dart (`lib/src/feature/health/`), `HealthConnect` per person: with writing on, every workout written as `healthRecordsOf` makes it (client IDs from the workout's ID, a version counted up as it changes, a fingerprint deciding whether it did), written again or deleted as the workouts change and as the app opens, what was written kept in the person's `health_connect.json` (`HealthStore`, never synced); with reading on, the sessions of the apps the person turned on brought in as workouts through the changes token (`importedWorkout` in `health_import.dart`, the workout's `origin` in its payload; a double of one Train has brings at most its heart rate, `isDouble`; one deleted in Train is remembered so it stays out; a track only when asked for on its page, `bringInTrack`), as the app opens and comes to the front (`HealthConnectListener`, which opens the rationale too); the screen from the account sheet (`HealthScreen`, `HealthCubit`). Tests on `test/support/fake_health_platform.dart`. Train needs Android 8 (API 26) for it.
- **The Polar Loop** (ADR 0030, D32 to D36) is Android's (`TrainPerson.loop` null elsewhere): `LoopBridge.kt` on Polar's BLE SDK 8.3.0 (from JitPack, a repository asked for Polar's group alone, its files pinned by SHA-256 in `android/gradle/verification-metadata.xml`; a new version needs its checksums there) over `app.halkyon.train/loop`, `…/loop/state` and `…/loop/search`, the first-time use's figures mapped and checked in `LoopSetup.kt` (tested on the JVM); `LoopPlatform` in Dart (`lib/src/feature/loop/`), `PolarLoop` per person: the band remembered in the person's `loop.json` (`LoopStore`, never synced) with what it was set up with and what it told, connected while the app is open, its clock set as each connection is ready, a changed profile offered on the band's screen and sent only when the person says (D35), *Let the band go* a factory reset after asking; `loopFiguresOf` fills Polar's defaults for the band alone (`loop_figures.dart`), the setup sheet asks for what the profile lacks with `ProfileForm(only: …)`. The person's body's figures are the synced, sealed `profile` (D33, *Your body*, `lib/src/feature/profile/`). Polar's SDK isn't free software (D36): the app's AGPL carries a section 7 permission for it (`halkyon_train_flutter/LICENSE-polar-exception.md`), its notices are in the licence list (`registerPolarLicense`, on Android), and an F-Droid listing of Train declares `NonFreeDep`. Never ask the SDK for `FEATURE_TELEMETRY`. Tests on `test/support/fake_loop_platform.dart`.
- **The icon** is Lucide's `bike` fading from cobalt through violet and strawberry into carrot (ADR 0031), drawn with the other apps' by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py train` from the root): `assets/icon/halkyon_train_icon.png`, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons. `registerLucideLicense` puts Lucide's notice in the licence list. - **The icon** is Lucide's `bike` fading from cobalt through violet and strawberry into carrot (ADR 0031), drawn with the other apps' by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py train` from the root): `assets/icon/halkyon_train_icon.png`, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons. `registerLucideLicense` puts Lucide's notice in the licence list.
- **`serverpod start`** runs in `apps/train/halkyon_train_server` (it launches the app from `lib/driver.dart`); `.mcp.json`'s `--server-dir` names it when a session works on Train. Its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them. Train's development server has ports of its own (API 8180, Insights 8181, web 8182, PostgreSQL 8190; the app's `assets/config.json`), so it runs beside Learn's, as a member of Learn's register; its tests use `config/test.yaml`'s ports, as Learn's and Todo's do, which Melos runs one package at a time. - **`serverpod start`** runs in `apps/train/halkyon_train_server` (it launches the app from `lib/driver.dart`); `.mcp.json`'s `--server-dir` names it when a session works on Train. Its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them. Train's development server has ports of its own (API 8180, Insights 8181, web 8182, PostgreSQL 8190; the app's `assets/config.json`), so it runs beside Learn's, as a member of Learn's register; its tests use `config/test.yaml`'s ports, as Learn's and Todo's do, which Melos runs one package at a time.

View file

@ -20,4 +20,6 @@ root, or `dart run tool/check.dart` for everything.
The app's icon is [Lucide](https://lucide.dev)'s `bike` (ISC licence, ADR 0031), drawn The app's icon is [Lucide](https://lucide.dev)'s `bike` (ISC licence, ADR 0031), drawn
by the repository's `tool/app_icons.py`. Licences: the server under the SSPL, the client by the repository's `tool/app_icons.py`. Licences: the server under the SSPL, the client
and the app under the AGPL ([`LICENSE.md`](../../LICENSE.md)). and the app under the AGPL, the app's with a permission for linking Polar's BLE SDK, which
isn't free software, for the Polar Loop (ADR 0030, D36;
[`LICENSE.md`](../../LICENSE.md)).

View file

@ -30,6 +30,7 @@
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
/android/build/
# Web related # Web related
lib/generated_plugin_registrant.dart lib/generated_plugin_registrant.dart

View file

@ -0,0 +1,22 @@
# Additional permission for Polar's BLE SDK
Train's app (`apps/train/halkyon_train_flutter`) is under the GNU Affero General Public
License, version 3 or any later version (`LICENSE`). Its Android build links Polar
Electro Oy's Polar BLE SDK for the Polar Loop (ADR 0030, D34, D36), which is under
Polar's *Software Development Kit Limited License Agreement*, not a free licence
(`assets/licences/Polar_SDK_License.txt`). For that, its licensors add this permission,
under section 7 of the GNU AGPL version 3:
> Additional permission under GNU AGPL version 3 section 7
>
> If you modify this Program, or any covered work, by linking or combining it with the
> Polar BLE SDK of Polar Electro Oy (or a modified version of that library), containing
> parts covered by the terms of Polar's Software Development Kit Limited License
> Agreement, the licensors of this Program grant you additional permission to convey the
> resulting work. Corresponding Source for a non-source form of such a combination
> shall include the source code for the parts of this Program used, but not for the
> parts of the Polar BLE SDK used.
The SDK's own terms still bind whoever conveys it: Polar's agreement allows its object
code to be distributed only together with an app and to move data between Polar's
devices and that app, and Polar may end it.

View file

@ -55,6 +55,8 @@ dependencies {
implementation("androidx.core:core:1.18.0") implementation("androidx.core:core:1.18.0")
// Health Connect (ADR 0030, D30, D31): AndroidX's client, no Play services. // Health Connect (ADR 0030, D30, D31): AndroidX's client, no Play services.
implementation("androidx.health.connect:connect-client:1.1.0") implementation("androidx.health.connect:connect-client:1.1.0")
// The Polar Loop (ADR 0030, D32, D34): Polar's BLE SDK, under Polar's licence.
implementation("com.github.polarofficial:polar-ble-sdk:8.3.0")
// The recorder's pure Kotlin parts (auto-pause) tested on the JVM. // The recorder's pure Kotlin parts (auto-pause) tested on the JVM.
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
} }

View file

@ -0,0 +1,180 @@
package app.halkyon.train
import android.content.Context
import com.polar.androidcommunications.api.ble.model.DisInfo
import com.polar.androidcommunications.api.ble.model.gatt.client.ChargeState
import com.polar.androidcommunications.api.ble.model.gatt.client.PowerSourcesState
import com.polar.sdk.api.PolarBleApi
import com.polar.sdk.api.PolarBleApiCallbackProvider
import com.polar.sdk.api.PolarBleApiDefaultImpl
import com.polar.sdk.api.model.PolarDeviceInfo
import com.polar.sdk.api.model.PolarHealthThermometerData
import com.polar.sdk.api.model.PolarHrData
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/**
* The Polar Loop for the app (ADR 0030, D32, D34), through Polar's BLE SDK,
* over `app.halkyon.train/loop`: Loops nearby (`…/loop/search`), connecting
* to the one the person chose, its first-time use, its clock, letting it go,
* and what it tells of itself as it connects (`…/loop/state`). The app
* decides when; this only speaks to the band.
*/
class LoopBridge(private val context: Context) : MethodChannel.MethodCallHandler {
private val scope: CoroutineScope = MainScope()
private var events: EventChannel.EventSink? = null
// The SDK made on first use, so that an app without a Loop never starts it.
// Its telemetry stays off: FEATURE_TELEMETRY is never asked for.
private val made = lazy {
PolarBleApiDefaultImpl.defaultImplementation(
context.applicationContext,
setOf(
PolarBleApi.PolarBleSdkFeature.FEATURE_DEVICE_INFO,
PolarBleApi.PolarBleSdkFeature.FEATURE_BATTERY_INFO,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_FILE_TRANSFER,
PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_CONTROL,
),
).apply {
setAutomaticReconnection(true)
setApiCallback(callback)
}
}
private val api: PolarBleApi by made
/** What the band tells, sent on as `{id, event, …}`. */
val state =
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, sink: EventChannel.EventSink) {
events = sink
}
override fun onCancel(arguments: Any?) {
events = null
}
}
/** Loops nearby while the setup sheet looks, each as `{id, name, rssi}`. */
val search =
object : EventChannel.StreamHandler {
private var looking: Job? = null
override fun onListen(arguments: Any?, sink: EventChannel.EventSink) {
looking?.cancel()
looking =
scope.launch {
try {
api.searchForDevice("Polar").collect { device ->
if (LoopSetup.isLoop(device.name)) {
sink.success(mapOf("id" to device.deviceId, "name" to device.name, "rssi" to device.rssi))
}
}
} catch (error: SecurityException) {
sink.error("refused", error.message, null)
} catch (error: Exception) {
sink.error("failed", error.message, null)
}
}
}
override fun onCancel(arguments: Any?) {
looking?.cancel()
looking = null
}
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
scope.launch {
try {
val id = call.argument<String>("id")!!
when (call.method) {
"connect" -> result.success(api.connectToDevice(id).let { null })
"disconnect" -> result.success(api.disconnectFromDevice(id).let { null })
"isSetUp" -> result.success(api.isFtuDone(id))
"setUp" -> {
val config = LoopSetup.configOf(call.argument<Map<String, Any?>>("figures")!!, ZonedDateTime.now())
api.doFirstTimeUse(id, config)
api.setLocalTime(id, LocalDateTime.now())
result.success(null)
}
"setClock" -> result.success(api.setLocalTime(id, LocalDateTime.now()).let { null })
"reset" -> result.success(api.doFactoryReset(id).let { null })
else -> result.notImplemented()
}
} catch (error: SecurityException) {
result.error("refused", error.message, null)
} catch (error: Exception) {
result.error("failed", error.message, null)
}
}
}
/** Lets the band go from the SDK as the app closes. */
fun dispose() {
scope.cancel()
events = null
if (made.isInitialized()) api.shutDown()
}
private fun send(id: String, event: String, vararg values: Pair<String, Any?>) {
events?.success(mapOf("id" to id, "event" to event, *values))
}
private val callback =
object : PolarBleApiCallbackProvider {
override fun blePowerStateChanged(powered: Boolean) {}
override fun deviceConnecting(polarDeviceInfo: PolarDeviceInfo) =
send(polarDeviceInfo.deviceId, "connecting")
override fun deviceConnected(polarDeviceInfo: PolarDeviceInfo) =
send(polarDeviceInfo.deviceId, "connected", "name" to polarDeviceInfo.name)
override fun deviceDisconnected(polarDeviceInfo: PolarDeviceInfo) =
send(polarDeviceInfo.deviceId, "disconnected")
override fun bleSdkFeatureReady(identifier: String, feature: PolarBleApi.PolarBleSdkFeature) {
// The clock and the first-time use go through the band's files.
if (feature == PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP) send(identifier, "ready")
}
override fun bleSdkFeaturesReadiness(
identifier: String,
ready: List<PolarBleApi.PolarBleSdkFeature>,
unavailable: List<PolarBleApi.PolarBleSdkFeature>,
) {}
override fun disInformationReceived(identifier: String, uuid: UUID, value: String) {
if (uuid == FIRMWARE_REVISION) send(identifier, "firmware", "version" to value)
}
override fun disInformationReceived(identifier: String, disInfo: DisInfo) {}
override fun batteryLevelReceived(identifier: String, level: Int) =
send(identifier, "battery", "level" to level)
override fun batteryChargingStatusReceived(identifier: String, chargingStatus: ChargeState) =
send(identifier, "charging", "charging" to (chargingStatus == ChargeState.CHARGING))
override fun powerSourcesStateReceived(identifier: String, powerSourcesState: PowerSourcesState) {}
override fun hrNotificationReceived(identifier: String, data: PolarHrData.PolarHrSample) {}
override fun htsNotificationReceived(identifier: String, data: PolarHealthThermometerData) {}
}
private companion object {
/** Device Information's Firmware Revision String (0x2A26). */
val FIRMWARE_REVISION: UUID = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb")
}
}

View file

@ -0,0 +1,57 @@
package app.halkyon.train
import com.polar.sdk.api.model.PolarFirstTimeUseConfig
import java.time.LocalDate
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
/**
* The app's figures for a Loop's first-time use (ADR 0030, D34) as Polar's
* SDK takes them: the app sends every one, its defaults filled in already;
* this only maps names and checks what the SDK would refuse.
*/
object LoopSetup {
/** Polar's six training backgrounds, by the app's names. */
val trainingBackgrounds =
mapOf(
"occasional" to 10,
"regular" to 20,
"frequent" to 30,
"heavy" to 40,
"semiPro" to 50,
"pro" to 60,
)
/** Whether a device found under [name] is a Loop. */
fun isLoop(name: String?): Boolean = name != null && name.trim().startsWith("Polar Loop", ignoreCase = true)
/** [figures] as the SDK's configuration, the band's clock at [now]; throws IllegalArgumentException where one is missing or out of range. */
fun configOf(figures: Map<String, Any?>, now: ZonedDateTime): PolarFirstTimeUseConfig {
fun number(key: String): Number = figures[key] as? Number ?: throw IllegalArgumentException("$key missing")
return PolarFirstTimeUseConfig(
gender =
when (figures["sex"]) {
"female" -> PolarFirstTimeUseConfig.Gender.FEMALE
"male" -> PolarFirstTimeUseConfig.Gender.MALE
else -> throw IllegalArgumentException("sex missing")
},
birthDate = LocalDate.parse(figures["birthDate"] as? String ?: throw IllegalArgumentException("birthDate missing")),
height = number("heightCm").toFloat(),
weight = number("weightKg").toFloat(),
maxHeartRate = number("maxHeartRate").toInt(),
vo2Max = number("vo2Max").toInt(),
restingHeartRate = number("restingHeartRate").toInt(),
trainingBackground =
trainingBackgrounds[figures["trainingBackground"]] ?: throw IllegalArgumentException("trainingBackground missing"),
deviceTime = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
typicalDay =
when (figures["typicalDay"]) {
"mostlySitting" -> PolarFirstTimeUseConfig.TypicalDay.MOSTLY_SITTING
"mostlyStanding" -> PolarFirstTimeUseConfig.TypicalDay.MOSTLY_STANDING
"mostlyMoving" -> PolarFirstTimeUseConfig.TypicalDay.MOSTLY_MOVING
else -> throw IllegalArgumentException("typicalDay missing")
},
sleepGoalMinutes = number("sleepGoalMinutes").toInt(),
)
}
}

View file

@ -17,6 +17,9 @@ class MainActivity : FlutterActivity() {
/** Health Connect's side (ADR 0030, D30, D31). */ /** Health Connect's side (ADR 0030, D30, D31). */
private val health by lazy { HealthConnectBridge(this) } private val health by lazy { HealthConnectBridge(this) }
/** The Polar Loop's side (ADR 0030, D34). */
private val loop by lazy { LoopBridge(this) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
health.onIntent(intent) health.onIntent(intent)
@ -71,6 +74,9 @@ class MainActivity : FlutterActivity() {
.setStreamHandler(DeviceScanner(this, RowerData.SERVICE, ::mayBeRower)) .setStreamHandler(DeviceScanner(this, RowerData.SERVICE, ::mayBeRower))
MethodChannel(messenger, "app.halkyon.train/health").setMethodCallHandler(health) MethodChannel(messenger, "app.halkyon.train/health").setMethodCallHandler(health)
EventChannel(messenger, "app.halkyon.train/health/rationale").setStreamHandler(health.rationaleRequests) EventChannel(messenger, "app.halkyon.train/health/rationale").setStreamHandler(health.rationaleRequests)
MethodChannel(messenger, "app.halkyon.train/loop").setMethodCallHandler(loop)
EventChannel(messenger, "app.halkyon.train/loop/state").setStreamHandler(loop.state)
EventChannel(messenger, "app.halkyon.train/loop/search").setStreamHandler(loop.search)
EventChannel(messenger, "app.halkyon.train/recorder/lines").setStreamHandler( EventChannel(messenger, "app.halkyon.train/recorder/lines").setStreamHandler(
object : EventChannel.StreamHandler { object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink) { override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
@ -90,6 +96,7 @@ class MainActivity : FlutterActivity() {
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) { override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
RecorderService.lines = null RecorderService.lines = null
health.dispose() health.dispose()
loop.dispose()
super.cleanUpFlutterEngine(flutterEngine) super.cleanUpFlutterEngine(flutterEngine)
} }

View file

@ -0,0 +1,84 @@
package app.halkyon.train
import com.polar.sdk.api.model.PolarFirstTimeUseConfig
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.ZonedDateTime
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
// A Loop's first-time use from the app's figures (ADR 0030, D34): Polar's
// names for the app's, and nothing sent that the SDK would refuse.
class LoopSetupTest {
private val now = ZonedDateTime.of(2026, 10, 28, 18, 30, 0, 0, ZoneOffset.ofHours(1))
private val figures =
mapOf<String, Any?>(
"sex" to "female",
"birthDate" to "1990-04-02",
"heightCm" to 172,
"weightKg" to 64.5,
"restingHeartRate" to 58,
"maxHeartRate" to 186,
"vo2Max" to 42,
"trainingBackground" to "semiPro",
"typicalDay" to "mostlyStanding",
"sleepGoalMinutes" to 480,
)
@Test
fun `the app's figures become Polar's`() {
val config = LoopSetup.configOf(figures, now)
assertEquals(PolarFirstTimeUseConfig.Gender.FEMALE, config.gender)
assertEquals(LocalDate.of(1990, 4, 2), config.birthDate)
assertEquals(172f, config.height)
assertEquals(64.5f, config.weight)
assertEquals(58, config.restingHeartRate)
assertEquals(186, config.maxHeartRate)
assertEquals(42, config.vo2Max)
assertEquals(50, config.trainingBackground)
assertEquals(PolarFirstTimeUseConfig.TypicalDay.MOSTLY_STANDING, config.typicalDay)
assertEquals(480, config.sleepGoalMinutes)
assertEquals("2026-10-28T18:30:00+01:00", config.deviceTime)
}
@Test
fun `each of the six training backgrounds is one the SDK takes`() {
for ((name, value) in LoopSetup.trainingBackgrounds) {
val config = LoopSetup.configOf(figures + ("trainingBackground" to name), now)
assertEquals(value, config.trainingBackground)
}
assertEquals(6, LoopSetup.trainingBackgrounds.size)
}
@Test
fun `a figure missing is refused`() {
for (key in figures.keys) {
assertThrows(key, IllegalArgumentException::class.java) {
LoopSetup.configOf(figures - key, now)
}
}
}
@Test
fun `a figure out of Polar's range is refused`() {
for ((key, value) in listOf("heightCm" to 250, "restingHeartRate" to 15, "sleepGoalMinutes" to 720)) {
assertThrows(key, IllegalArgumentException::class.java) {
LoopSetup.configOf(figures + (key to value), now)
}
}
}
@Test
fun `only a Loop is taken for one`() {
assertTrue(LoopSetup.isLoop("Polar Loop 1A2B3C4D"))
assertTrue(LoopSetup.isLoop("POLAR LOOP"))
assertFalse(LoopSetup.isLoop("Polar 360 1A2B3C4D"))
assertFalse(LoopSetup.isLoop("Polar H10 1A2B3C4D"))
assertFalse(LoopSetup.isLoop(null))
}
}

View file

@ -2,6 +2,12 @@ allprojects {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
// Polar's BLE SDK for the Loop (ADR 0030, D34), which Polar publishes
// through JitPack alone: asked for Polar's package and nothing else.
exclusiveContent {
forRepository { maven("https://jitpack.io") }
filter { includeGroup("com.github.polarofficial") }
}
} }
} }

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Polar's BLE SDK comes from JitPack (ADR 0030, D34), which builds it from
Polar's repository itself: its files are pinned here by their SHA-256, as
JitPack served them on 28 Oct 2026, so that a file changed there fails the
build. Everything else comes from Google's and Maven Central's repositories
and is trusted as before.
-->
<verification-metadata xmlns="https://schema.gradle.org/dependency-verification"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">
<configuration>
<verify-metadata>true</verify-metadata>
<verify-signatures>false</verify-signatures>
<trusted-artifacts>
<trust group="^(?!com[.]github[.]polarofficial$).*$" regex="true"/>
</trusted-artifacts>
</configuration>
<components>
<component group="com.github.polarofficial" name="polar-ble-sdk" version="8.3.0">
<artifact name="polar-ble-sdk-8.3.0.aar">
<sha256 value="f207c8b4a1812890e4737ac5c72b46bbcb1889826ef89d82ddc66c5772a826d0" origin="JitPack, 28 Oct 2026"/>
</artifact>
<artifact name="polar-ble-sdk-8.3.0.module">
<sha256 value="00ddeeac9e681cdfa411dd4cb6834968c0b08c18efff7debbd9d6379200293fc" origin="JitPack, 28 Oct 2026"/>
</artifact>
<artifact name="polar-ble-sdk-8.3.0.pom">
<sha256 value="1bceb6bf7e24262b196a3a7d55b94dadbfbe9428fe5aa998e20d4e6b4a4e1285" origin="JitPack, 28 Oct 2026"/>
</artifact>
</component>
</components>
</verification-metadata>

View file

@ -0,0 +1,92 @@
PLEASE READ THE FOLLOWING LICENSE AGREEMENT TERMS AND CONDITIONS CAREFULY BEFORE DOWNLOADING OR USING THE POLAR SOFTWARE DEVELOPMENT KIT SOFTWARE. THESE TERMS AND CONDITIONS CONSTITUTE A LEGAL AGREEMENT BETWEEN YOU AND POLAR ELECTRO.
SOFTWARE DEVELOPMENT KIT LIMITED LICENSE AGREEMENT
Quick Summary / Your rights to use the SDK
You may use, copy and modify the SDK as long as you include the original copyright and license notice in any copy of the software/source and you comply with the license terms. You are allowed to use the SDK for the development of software for your private as well as for commercial use for as long as you use the SDK in compliance with the license terms.
1 Purpose
You would like to use the Licensed Material (as defined below) to develop one or more Licensee Product(s) (as defined below). Polar is willing to grant You a limited license to use the Polar Software Development Kit software to develop, test and commercialize (if You so select) Your Licensee Software on the terms and conditions set forth herein.
2 Definitions
In this Agreement, unless the context requires otherwise, the following words and phrases shall have the following meanings:
"Affiliate" shall mean an entity (i) which is directly or indirectly controlling such Party; (ii) which is under the same direct or indirect ownership or control as such Party; or (iii) which is directly or indirectly owned or controlled by such Party. For the purposes of this Agreement, an entity shall be treated as being controlled by another if that other entity; (i) has fifty percent (50%) or more of the votes in such entity; or (ii) is able to direct its affairs and/or to control the composition of its board of directors or equivalent body.
"Agreement" shall mean this Limited License Agreement, its Appendices and any amendments and extensions thereof.
"Confidential Information" shall mean all material and information in any form received from a Party hereto, its business partners including, but not limited to, know-how and/or information on any Intellectual Property Rights, technical or commercial matters, business operations, facilities, products, techniques and processes, Licensed Materials and its copies, as well as all translations of computer programs contained in the Licensed Materials to different computer programming languages and/or spoken languages.
"Data" shall mean data made available through any Polar devices or services to Licensee. Data may consist of but is not limited to heart rate training data, activity data, program data and/or user settings and/or personal data of the end-user is using a Polar device from which the data is being transferred to Licensee Product. Data may consist data which is proprietary to Polar.
"Effective Date" shall mean the date when You have accepted these terms and conditions and/or accessed the Licensed Materials.
"Intellectual Property Rights" shall mean any and all patent, copyright, trademark, design right, petty patent, chip topography, service mark, domain-name or any other right or trade secret whether registered or not.
"Licensed Materials" shall mean Polar's software development kit, as amended from time to time, enabling development of applications which read Data directly from Polar's devices.
"Licensee Products" means Licensee's products that interoperate with the Licensed Materials or parts of Licensed Materials and Polar's products.
"Logos" shall mean a graphic design, stylized name, unique symbol, brand name, "Polar", Polar logo, Polar's product names or any other trademarks or trade names, whether registered or not, of Polar.
"Polar" shall mean Polar Electro Oy, a company organized under the laws of Finland and having its registered office at Professorintie 5, 90440 Kempele, Finland.
"You" and "Your" shall mean and refers to the person(s) or legal entity (including but not limited to a company, organization, educational institution, or governmental agency, instrumentality or department) using the Licensed Materials or otherwise exercising rights under this Agreement.
3 Grant of License
3.1 Under the terms of this Agreement, POLAR grants You a worldwide, non-exclusive, non-transferable limited license to use the Licensed Materials and copy and distribute object code form of the Licensed Materials together with Developer Software to End-Users solely for purpose of enabling Data transfer between POLAR product and Developer Software.
3.2 Licensed Materials and Polar's devices are not intended to be used in life critical, life supporting or medical purpose. Consequently, use of any Data in any life critical, life supporting or medical purpose or develop or have developed any application to enable such use of the Data, or develop any applications which would modify or use the Data for any purposes other than described in this Agreement shall be at Your own risk. Polar specifically denies any and all liability on whatever ground for the use of Licensed Materials and/or Data for above mentioned purposes, and you shall indemnify and hold harmless Polar and its Affiliates for any claims, suits, actions, demands, liabilities, costs, fees, penalties and damages of whatever nature related to such use of the Licensed Materials and/or Data.
3.3 You shall be responsible of the proper handling of Data, taking into consideration all applicable laws and regulations, including but not limited to the applicable privacy and data protection laws. It is on Your responsibility and you represent and warrant to Polar that you will be handling personal data (including Data) in compliance with applicable data protection laws.
4 Ownership, Copyright and Trademarks
4.1 Polar and its licensors shall continue to own all right, title and interest, including copyright and all other intellectual property rights in and to the Licensed Materials, other Confidential Information and Logos.
4.2 You may give feedback on Licensed Materials to Polar. Any input, suggestions or feedback of a technical nature on Licensed Materials, Polar products or any service offered by Polar ("Feedback") shall be given on non-confidential basis to Polar and You hereby waive any confidentiality restrictions for such Feedback. In addition, You shall grant to Polar and its affiliates a worldwide, non-exclusive, perpetual, irrevocable, sub-licensable, royalty-free right and license under Your copyrights to copy, reproduce, modify, create derivative works and directly or indirectly distribute, make available and communicate to public the Feedback in or in connection to any Polar products, software and/or services. For the avoidance of doubt, Feedback to Polar shall be considered as Confidential Information of Polar.
4.3 You agree to make adequate copyright markings to the Licensee Products regarding the use of the Licensed Materials.
4.4 You may not use POLAR, Polar logo, POLAR's product names or any other trademarks or trade names, whether registered or not, of POLAR, or any similar mark or name, as a Developer Software trademark or trade name or for its marketing or advertising purposes, or indicate any association with Polar, express or implied, without Polar's specific written consent.
4.5 You will not at any time do or cause to be done any such act or thing which in any way impairs, or intends to impair, any right, title, interest or any Intellectual Property Right of POLAR or it's licensors. You shall not in any manner represent that it has any ownership of any kind in any of the above mentioned Intellectual Property Rights.
5 Technical Support and Updates
5.1 This Agreement does not contain any technical support or maintenance services.
5.2 Polar shall not have any obligations to provide any updates or new versions of the Licensed Materials, or maintenance or support for the Licensed Materials. Polar shall, however, have the right, within its sole discretion, to provide updates or new versions and offer support services. In such case, payable fees and/or separate terms of use, if any, shall be agreed separately. Polar may also choose, at is sole discretion, to make available support services to selected licensees of the Licensed Materials, for charge or free of charge.
6 Fees
Licensed Materials shall be offered free of charge, subject to You being in compliance with the terms of this Agreement at all times.
7 No Warranty
THE LICENSED MATERIALS AND CONFIDENTIAL INFORMATION ARE PROVIDED ON AN "AS IS" BASIS WITHOUT WARRANTY OF ANY KIND, WHETHER EXPRESS OR IMPLIED, AND NEITHER POLAR, ITS LICENSORS, SUPPLIERS OR AFFILIATES NOR THE TITLE AND/OR INTELLECTUAL PROPERTY RIGHT HOLDERS MAKE ANY REPRESENTATIONS OR WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE OR THAT THE LICENSED MATERIALS OR CONFIDENTIAL INFORMATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR ANY OTHER RIGHTS. THERE IS NO WARRANTY BY POLAR OR BY ANY OTHER PARTY THAT THE FUNCTIONS AND SOLUTIONS CONTAINED OR OUTLINED IN THE LICENSED MATERIALS OR CONFIDENTIAL INFORMATION WILL MEET YOUR REQUIREMENTS OR WILL OPERATE UNINTERRUPTED OR ERROR-FREE. YOU ASSUME ALL RESPONSIBILITY AND RISK FOR THE SELECTION OF THE LICENSED MATERIALS TO ACHIEVE YOUR INTENDED RESULTS AND FOR THE INSTALLATION, USE AND RESULTS OBTAINED FROM IT. IN THE EVENT THE WARRANTY EXCLUSION IS NOT VALID IN SOME JURISDICTION, YOU HEREBY ACKNOWLEDGE AND AGREE THAT THE WARRANTY EXCLUSION SET FORTH IN THIS SECTION 6 SHALL BE APPLIED TO THE MAXIMUM EXTENT PERMITTED BY THE APPLICABLE LAW.
8 Limitation of Liability
EXCEPT FOR DAMAGES CAUSED BY WILFUL ACTS OR GROSS NEGLIGENCE, IN NO EVENT SHALL POLAR, ITS EMPLOYEES, DIRECTORS, SUPPLIERS, LICENSORS OR THEIR AFFILIATES BE LIABLE FOR ANY LOST PROFITS, REVENUE, SALES, DATA OR COSTS OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, PROPERTY DAMAGE, PERSONAL INJURY, INTERRUPTION OF BUSINESS, LOSS OF BUSINESS INFORMATION OR FOR ANY SPECIAL, DIRECT, INDIRECT, INCIDENTAL, ECONOMIC, PUNITIVE, SPECIAL OR CONSEQUENTIAL DAMAGES, HOWEVER CAUSED OR ANY DAMAGES ARISING OUT OF OR RELATING TO AN INFRINGEMENT OR ALLEGED INFRINGEMENT OF THIRD PARTY INTELLECTUAL PROPERTY RIGHTS OR CONFIDENTIAL INFORMATION, AND WHETHER ARISING UNDER CONTRACT, TORT, NEGLIGENCE, OR OTHER THEORY OF LIABILITY ARISING HEREUNDER OR OUT OF THE USE OF OR INABILITY TO USE THE LICENSED MATERIALS OR CONFIDENTIAL INFORMATION, EVEN IF POLAR OR ITS EMPLOYEES OR SUPPLIERS, LICENSORS OR AFFILIATES ARE ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN THE EVENT THE ABOVE LIMITATION OF LIABILITY WOULD NOT BE ENFORCEABLE, POLAR'S, ITS EMPLOYEES', DIRECTORS' OR SUPPLIERS', LICENSORS' OR AFFILIATES' LIABILITY SHALL BE LIMITED TO THE MAXIMUM EXTENT PERMITTED BY THE APPLICABLE LAW. TO THE EXTENT PERMITTED BY APPLICABLE LAW, POLAR LIABILITY UNDER THIS AGREEMENT SHALL BE LIMITED TO 50 EUROS.
9 Confidentiality
9.1 You shall not disclose to third parties nor use for any purpose other than for the proper fulfillment of the purpose of this Agreement any Confidential Information possibly received from Polar in whatever form under or in connection with this Agreement without the prior written permission of Polar save for Confidential Information which
a) was in the public domain at the time of disclosure or later became part of the public domain without breach of the confidentiality obligations herein contained; or
b) was disclosed by a third party without breach of any obligation of confidentiality owed to Polar; or
c) was independently developed by Your personnel having no access to the Information, or
d) was ordered to be disclosed by a court of competent jurisdiction after giving notice to Polar.
9.2 You shall limit access to Confidential Information to those of Your personnel for whom such access is reasonably necessary for the proper performance of this Agreement and obtain written undertakings of confidentiality from them.
9.3 In addition to above, You agree not to imply to third parties existence of any business relationship, partnership or affiliation with Polar or use Polar as a reference without Polar's prior written consent.
10 Term and termination
10.1 This Agreement and the licenses granted under this Agreement become effective on the Effective Date.
10.2 This Agreement shall remain in force until terminated by Polar by either Party by providing a ninety (90) days prior written notice to the other Party.
10.3 Polar has the right, in addition and without prejudice to any other rights or remedies, to terminate this Agreement immediately as follows:
10.3.1 for any material breach of Agreement, that is not cured within seven (7) days of receipt by You of a written notice specifying the breach and requiring its cure;
10.3.2 upon receiving a written notice, if (a) all or a substantial portion of that Your assets are transferred to an assignee for the benefit of creditors, or to a receiver or a trustee in bankruptcy, (b) a proceeding is commenced by or against You for relief under bankruptcy or similar laws and such proceeding is not dismissed within sixty (60) days, or (c) the You are adjudged bankrupt.
10.4 If and when this Agreement is terminated due to any reason or cause whatsoever, then You shall cease the use of Licensed Materials and Confidential Information and any of its rights under Section 3, and destroy and delete all copies of the Licensed Materials and Confidential Information.
10.5 Upon termination of this Agreement, provisions of the Agreement which, by their nature, are intended to survive its termination or expiration, shall survive its termination or expiration.
11 Licensee Product and Indemnification
11.1 Licensee Product
You will be solely responsible for developing Licensee Product and that Licensee Product and any related products are safe, free of defects in design, materials and workmanship, and comply with applicable laws and regulations; and for testing, labeling, distributing, promoting, selling, and if necessary, recalling its products; and any costs related to the aforementioned actions. Polar's possible approval of the Licensee Product will not constitute endorsement of the fitness for a particular purpose or that it will work properly.
11.2 Your Indemnification
You agree to indemnify, defend, and hold Polar its agents, affiliates, and licensors harmless from any liability incurred by Polar, or claims, causes of action, damages, costs, fines, fees or expenses (including reasonable attorney's fees) asserted against Polar, by reason of or related third party claims, suits, actions, demands and proceedings for:
i) product liability, product safety and security, personal injury and / or death;
ii) loss of and / or damage to property; and / or
iii) alleged and / or actual infringement of any patent, right of patent, trade marks, copyrights or designs or other industrial or intellectual property rights,
which are attributable to You, or the use of any Intellectual Property Rights of Polar by You.
You shall also indemnify, defend and hold Polar, its agents, affiliates, and licensors harmless from any claim, costs, losses, damages, liabilities, judgments and expenses (including reasonable fees of attorneys and other professionals), arising out of or in connection with any claims arising out of or related to: (i) the Licensee Product (ii) use of the Licensee Product; (iii) use of the Licensee Product in combination with Licensed Materials or any other Polar device or service; or (iv) Your (or any user of the Licensee Product) use of Data in any manner inconsistent with or in breach of the terms of this Agreement or applicable laws.
12 Miscellaneous
12.1 No Waiver
The failure of Polar to exercise any of its rights under this Agreement or to require the performance of any term or provision of this Agreement, or the waiver by Polar of such breach of this Agreement, shall not prevent a subsequent exercise or enforcement of such right or be deemed a waiver of any subsequent breach of the same or any other term or provision of this Agreement. Any waiver of the performance of any of the terms or conditions of this Agreement shall be effective only if in writing and signed by Polar.
12.2 Headings
The headings in this Agreement are for the convenience only, and are not intended to define or limit the scope or interpretation of the Agreement or any provision hereof.
12.3 Severability
If any term of this Agreement is invalid or unenforceable, such terms or provisions shall not invalidate the rest of the Agreement which shall remain in full force and effect as if such invalidated or unenforceable terms or conditions had not been made a part of this Agreement, and an invalid or unenforceable term must be replaced by a term which reflects the economic intent of the terms or provisions found invalid or unenforceable.
12.4 Export Control
Licensed Material may be subject to import and export controls in other countries. You agree to strictly comply with all applicable import and export regulations and acknowledge that the Licensee has the responsibility to obtain licenses export, re-export, transfer or import the Licensed Material.
12.5 Entire Agreement and Assignment
This Agreement sets forth the entire agreement between the You and Polar with respect to the subject matter hereof and supersedes any prior proposals and representations, whether written or oral. You shall not have the right to assign this Agreement to a third party without the prior written consent of Polar. Polar shall have the right to assign this Agreement and all of the rights and obligations contained therein to a Licensee belonging to the same group of companies as Polar, and also to a third party within the sole discretion of Polar.
12.6 Applicable law and settlement of disputes
This Agreement and its terms and conditions shall be governed exclusively by and construed according to the laws of Finland. The official text of the Agreement or any notices given or accounts or statements required hereby shall be in English.
Any dispute or controversy or claim arising out of or relating to this Agreement involving the Parties, shall be settled in Finland, with Oulu court of justice as its first instance, in the English language and the process as well as the results of the proceedings shall be considered as confidential. Polar shall also have the right to seek for an injunction against You in any competent court of law in order to protect its rights and interests hereunder.

View file

@ -0,0 +1,288 @@
============================================================================
Protocol Buffers
============================================================================
// Protocol Buffers - Google's data interchange format
// Copyright 2015 Google Inc. All rights reserved.
// https://developers.google.com/protocol-buffers/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
============================================================================
RxSwift,RxAtomic
============================================================================
The MIT License Copyright © 2015 Krunoslav Zaher All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
============================================================================
RxJava
============================================================================
Copyright (c) 2016-present, RxJava Contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
============================================================================
RxAndroid
============================================================================
Copyright 2015 The RxAndroid authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
============================================================================
Apache 2.0
============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -188,5 +188,37 @@
"profileSleepGoal": "Schlafziel", "profileSleepGoal": "Schlafziel",
"profileSave": "Speichern", "profileSave": "Speichern",
"profileSaved": "Gespeichert", "profileSaved": "Gespeichert",
"profileInvalid": "Prüfe die markierten Angaben." "profileInvalid": "Prüfe die markierten Angaben.",
"profileNeeded": "Braucht dein Band",
"loopTitle": "Polar Loop",
"loopHint": "Dein Band: Einrichtung, Akku und Uhrzeit",
"loopNone": "Train richtet einen Polar Loop ein und betreut ihn selbst, ohne Polar Flow und ohne Polar-Konto. Ein Band, das mit Polar Flow verbunden war, braucht zuerst einen Werksreset.",
"loopSetUp": "Loop einrichten",
"loopSetUpAgain": "Neu einrichten",
"loopLetGo": "Band freigeben",
"loopLetGoTitle": "Band freigeben?",
"loopLetGoText": "Das Band wird auf Werkseinstellungen zurückgesetzt, und was es hält und noch nicht bei Train angekommen ist, geht verloren. Danach kannst du es mit einem anderen Handy oder mit Polar Flow koppeln.",
"loopLetGoConfirm": "Band zurücksetzen",
"loopConnected": "Verbunden",
"loopLastSeen": "Zuletzt gesehen {when}",
"loopNotInReach": "Nicht in Reichweite",
"loopBattery": "Akku {level} %",
"loopCharging": "Akku {level} %, lädt",
"loopFirmware": "Firmware {version}",
"loopProfileChanged": "Dein Körper hat sich geändert, seit das Band eingerichtet wurde",
"loopProfileChangedHint": "Senden richtet das Band mit deinen jetzigen Angaben neu ein.",
"loopSend": "Ans Band senden",
"loopSent": "Ans Band gesendet.",
"loopFailed": "Das Band hat nicht geantwortet. Halte es nah und versuch es noch mal.",
"loopSearching": "Suche Loops in der Nähe",
"loopSearchingHint": "Halte das Band nah; tipp darauf, um es zu wecken.",
"loopRefused": "Ohne Geräte in der Nähe kann Train dein Band nicht suchen. Erlaube sie in den Android-Einstellungen.",
"loopUnnamed": "Polar Loop",
"loopSettingUp": "Richte das Band ein …",
"loopSettingUpHint": "Android fragt dich vielleicht, ob du es koppeln willst.",
"loopSetUpDone": "Das Band ist eingerichtet.",
"loopFormTitle": "Für dein Band",
"loopFormIntro": "Dein Band braucht diese Angaben. Sie bleiben in „Dein Körper“, mit deinem Schlüssel versiegelt. Für einen Ruhe- oder Maximalpuls oder eine VO2max, die du nicht angegeben hast, nimmt das Band Polars Standardwerte, und „Dein Körper“ bleibt, wie er ist.",
"loopFormSave": "Band einrichten"
} }

View file

@ -239,5 +239,42 @@
"profileSleepGoal": "Sleep goal", "profileSleepGoal": "Sleep goal",
"profileSave": "Save", "profileSave": "Save",
"profileSaved": "Saved", "profileSaved": "Saved",
"profileInvalid": "Check the figures marked." "profileInvalid": "Check the figures marked.",
"profileNeeded": "Needed for your band",
"loopTitle": "Polar Loop",
"loopHint": "Your band: its setup, battery and clock",
"@loopHint": {"description": "Under Polar Loop in the account sheet."},
"loopNone": "Train sets up a Polar Loop and keeps it itself, without Polar Flow or a Polar account. A band used with Polar Flow needs a factory reset first.",
"loopSetUp": "Set up a Loop",
"loopSetUpAgain": "Set up again",
"loopLetGo": "Let the band go",
"loopLetGoTitle": "Let the band go?",
"loopLetGoText": "The band is reset to the factory's state, and what it holds that hasn't reached Train is lost. Afterwards it can be paired with another phone, or with Polar Flow.",
"loopLetGoConfirm": "Reset the band",
"loopConnected": "Connected",
"loopLastSeen": "Last seen {when}",
"@loopLastSeen": {"placeholders": {"when": {"type": "String", "example": "28 Oct 2026, 18:30"}}},
"loopNotInReach": "Not in reach",
"loopBattery": "Battery {level} %",
"@loopBattery": {"placeholders": {"level": {"type": "int"}}},
"loopCharging": "Battery {level} %, charging",
"@loopCharging": {"placeholders": {"level": {"type": "int"}}},
"loopFirmware": "Firmware {version}",
"@loopFirmware": {"placeholders": {"version": {"type": "String", "example": "1.2.3"}}},
"loopProfileChanged": "Your body changed since the band was set up",
"loopProfileChangedHint": "Sending sets the band up again with your figures as they are now.",
"loopSend": "Send to the band",
"loopSent": "Sent to the band.",
"loopFailed": "The band didn't answer. Keep it close, and try again.",
"loopSearching": "Looking for Loops nearby",
"loopSearchingHint": "Keep the band close; tap it to wake it up.",
"loopRefused": "Train can't look for your band without nearby devices. Allow them in Android's settings.",
"loopUnnamed": "Polar Loop",
"loopSettingUp": "Setting up the band…",
"loopSettingUpHint": "Android may ask you to pair it.",
"loopSetUpDone": "The band is set up.",
"loopFormTitle": "For your band",
"loopFormIntro": "Your band needs these. They're kept in Your body, sealed with your key. For a resting or highest heart rate or a VO2max you haven't given, the band takes Polar's defaults, and Your body stays as it is.",
"loopFormSave": "Set up the band"
} }

View file

@ -1161,6 +1161,192 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Check the figures marked.'** /// **'Check the figures marked.'**
String get profileInvalid; String get profileInvalid;
/// No description provided for @profileNeeded.
///
/// In en, this message translates to:
/// **'Needed for your band'**
String get profileNeeded;
/// No description provided for @loopTitle.
///
/// In en, this message translates to:
/// **'Polar Loop'**
String get loopTitle;
/// Under Polar Loop in the account sheet.
///
/// In en, this message translates to:
/// **'Your band: its setup, battery and clock'**
String get loopHint;
/// No description provided for @loopNone.
///
/// In en, this message translates to:
/// **'Train sets up a Polar Loop and keeps it itself, without Polar Flow or a Polar account. A band used with Polar Flow needs a factory reset first.'**
String get loopNone;
/// No description provided for @loopSetUp.
///
/// In en, this message translates to:
/// **'Set up a Loop'**
String get loopSetUp;
/// No description provided for @loopSetUpAgain.
///
/// In en, this message translates to:
/// **'Set up again'**
String get loopSetUpAgain;
/// No description provided for @loopLetGo.
///
/// In en, this message translates to:
/// **'Let the band go'**
String get loopLetGo;
/// No description provided for @loopLetGoTitle.
///
/// In en, this message translates to:
/// **'Let the band go?'**
String get loopLetGoTitle;
/// No description provided for @loopLetGoText.
///
/// In en, this message translates to:
/// **'The band is reset to the factory\'s state, and what it holds that hasn\'t reached Train is lost. Afterwards it can be paired with another phone, or with Polar Flow.'**
String get loopLetGoText;
/// No description provided for @loopLetGoConfirm.
///
/// In en, this message translates to:
/// **'Reset the band'**
String get loopLetGoConfirm;
/// No description provided for @loopConnected.
///
/// In en, this message translates to:
/// **'Connected'**
String get loopConnected;
/// No description provided for @loopLastSeen.
///
/// In en, this message translates to:
/// **'Last seen {when}'**
String loopLastSeen(String when);
/// No description provided for @loopNotInReach.
///
/// In en, this message translates to:
/// **'Not in reach'**
String get loopNotInReach;
/// No description provided for @loopBattery.
///
/// In en, this message translates to:
/// **'Battery {level} %'**
String loopBattery(int level);
/// No description provided for @loopCharging.
///
/// In en, this message translates to:
/// **'Battery {level} %, charging'**
String loopCharging(int level);
/// No description provided for @loopFirmware.
///
/// In en, this message translates to:
/// **'Firmware {version}'**
String loopFirmware(String version);
/// No description provided for @loopProfileChanged.
///
/// In en, this message translates to:
/// **'Your body changed since the band was set up'**
String get loopProfileChanged;
/// No description provided for @loopProfileChangedHint.
///
/// In en, this message translates to:
/// **'Sending sets the band up again with your figures as they are now.'**
String get loopProfileChangedHint;
/// No description provided for @loopSend.
///
/// In en, this message translates to:
/// **'Send to the band'**
String get loopSend;
/// No description provided for @loopSent.
///
/// In en, this message translates to:
/// **'Sent to the band.'**
String get loopSent;
/// No description provided for @loopFailed.
///
/// In en, this message translates to:
/// **'The band didn\'t answer. Keep it close, and try again.'**
String get loopFailed;
/// No description provided for @loopSearching.
///
/// In en, this message translates to:
/// **'Looking for Loops nearby'**
String get loopSearching;
/// No description provided for @loopSearchingHint.
///
/// In en, this message translates to:
/// **'Keep the band close; tap it to wake it up.'**
String get loopSearchingHint;
/// No description provided for @loopRefused.
///
/// In en, this message translates to:
/// **'Train can\'t look for your band without nearby devices. Allow them in Android\'s settings.'**
String get loopRefused;
/// No description provided for @loopUnnamed.
///
/// In en, this message translates to:
/// **'Polar Loop'**
String get loopUnnamed;
/// No description provided for @loopSettingUp.
///
/// In en, this message translates to:
/// **'Setting up the band…'**
String get loopSettingUp;
/// No description provided for @loopSettingUpHint.
///
/// In en, this message translates to:
/// **'Android may ask you to pair it.'**
String get loopSettingUpHint;
/// No description provided for @loopSetUpDone.
///
/// In en, this message translates to:
/// **'The band is set up.'**
String get loopSetUpDone;
/// No description provided for @loopFormTitle.
///
/// In en, this message translates to:
/// **'For your band'**
String get loopFormTitle;
/// No description provided for @loopFormIntro.
///
/// In en, this message translates to:
/// **'Your band needs these. They\'re kept in Your body, sealed with your key. For a resting or highest heart rate or a VO2max you haven\'t given, the band takes Polar\'s defaults, and Your body stays as it is.'**
String get loopFormIntro;
/// No description provided for @loopFormSave.
///
/// In en, this message translates to:
/// **'Set up the band'**
String get loopFormSave;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -637,4 +637,114 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get profileInvalid => 'Prüfe die markierten Angaben.'; String get profileInvalid => 'Prüfe die markierten Angaben.';
@override
String get profileNeeded => 'Braucht dein Band';
@override
String get loopTitle => 'Polar Loop';
@override
String get loopHint => 'Dein Band: Einrichtung, Akku und Uhrzeit';
@override
String get loopNone =>
'Train richtet einen Polar Loop ein und betreut ihn selbst, ohne Polar Flow und ohne Polar-Konto. Ein Band, das mit Polar Flow verbunden war, braucht zuerst einen Werksreset.';
@override
String get loopSetUp => 'Loop einrichten';
@override
String get loopSetUpAgain => 'Neu einrichten';
@override
String get loopLetGo => 'Band freigeben';
@override
String get loopLetGoTitle => 'Band freigeben?';
@override
String get loopLetGoText =>
'Das Band wird auf Werkseinstellungen zurückgesetzt, und was es hält und noch nicht bei Train angekommen ist, geht verloren. Danach kannst du es mit einem anderen Handy oder mit Polar Flow koppeln.';
@override
String get loopLetGoConfirm => 'Band zurücksetzen';
@override
String get loopConnected => 'Verbunden';
@override
String loopLastSeen(String when) {
return 'Zuletzt gesehen $when';
}
@override
String get loopNotInReach => 'Nicht in Reichweite';
@override
String loopBattery(int level) {
return 'Akku $level %';
}
@override
String loopCharging(int level) {
return 'Akku $level %, lädt';
}
@override
String loopFirmware(String version) {
return 'Firmware $version';
}
@override
String get loopProfileChanged =>
'Dein Körper hat sich geändert, seit das Band eingerichtet wurde';
@override
String get loopProfileChangedHint =>
'Senden richtet das Band mit deinen jetzigen Angaben neu ein.';
@override
String get loopSend => 'Ans Band senden';
@override
String get loopSent => 'Ans Band gesendet.';
@override
String get loopFailed =>
'Das Band hat nicht geantwortet. Halte es nah und versuch es noch mal.';
@override
String get loopSearching => 'Suche Loops in der Nähe';
@override
String get loopSearchingHint =>
'Halte das Band nah; tipp darauf, um es zu wecken.';
@override
String get loopRefused =>
'Ohne Geräte in der Nähe kann Train dein Band nicht suchen. Erlaube sie in den Android-Einstellungen.';
@override
String get loopUnnamed => 'Polar Loop';
@override
String get loopSettingUp => 'Richte das Band ein …';
@override
String get loopSettingUpHint =>
'Android fragt dich vielleicht, ob du es koppeln willst.';
@override
String get loopSetUpDone => 'Das Band ist eingerichtet.';
@override
String get loopFormTitle => 'Für dein Band';
@override
String get loopFormIntro =>
'Dein Band braucht diese Angaben. Sie bleiben in „Dein Körper“, mit deinem Schlüssel versiegelt. Für einen Ruhe- oder Maximalpuls oder eine VO2max, die du nicht angegeben hast, nimmt das Band Polars Standardwerte, und „Dein Körper“ bleibt, wie er ist.';
@override
String get loopFormSave => 'Band einrichten';
} }

View file

@ -633,6 +633,114 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get profileInvalid => 'Check the figures marked.'; String get profileInvalid => 'Check the figures marked.';
@override
String get profileNeeded => 'Needed for your band';
@override
String get loopTitle => 'Polar Loop';
@override
String get loopHint => 'Your band: its setup, battery and clock';
@override
String get loopNone =>
'Train sets up a Polar Loop and keeps it itself, without Polar Flow or a Polar account. A band used with Polar Flow needs a factory reset first.';
@override
String get loopSetUp => 'Set up a Loop';
@override
String get loopSetUpAgain => 'Set up again';
@override
String get loopLetGo => 'Let the band go';
@override
String get loopLetGoTitle => 'Let the band go?';
@override
String get loopLetGoText =>
'The band is reset to the factory\'s state, and what it holds that hasn\'t reached Train is lost. Afterwards it can be paired with another phone, or with Polar Flow.';
@override
String get loopLetGoConfirm => 'Reset the band';
@override
String get loopConnected => 'Connected';
@override
String loopLastSeen(String when) {
return 'Last seen $when';
}
@override
String get loopNotInReach => 'Not in reach';
@override
String loopBattery(int level) {
return 'Battery $level %';
}
@override
String loopCharging(int level) {
return 'Battery $level %, charging';
}
@override
String loopFirmware(String version) {
return 'Firmware $version';
}
@override
String get loopProfileChanged =>
'Your body changed since the band was set up';
@override
String get loopProfileChangedHint =>
'Sending sets the band up again with your figures as they are now.';
@override
String get loopSend => 'Send to the band';
@override
String get loopSent => 'Sent to the band.';
@override
String get loopFailed =>
'The band didn\'t answer. Keep it close, and try again.';
@override
String get loopSearching => 'Looking for Loops nearby';
@override
String get loopSearchingHint => 'Keep the band close; tap it to wake it up.';
@override
String get loopRefused =>
'Train can\'t look for your band without nearby devices. Allow them in Android\'s settings.';
@override
String get loopUnnamed => 'Polar Loop';
@override
String get loopSettingUp => 'Setting up the band…';
@override
String get loopSettingUpHint => 'Android may ask you to pair it.';
@override
String get loopSetUpDone => 'The band is set up.';
@override
String get loopFormTitle => 'For your band';
@override
String get loopFormIntro =>
'Your band needs these. They\'re kept in Your body, sealed with your key. For a resting or highest heart rate or a VO2max you haven\'t given, the band takes Polar\'s defaults, and Your body stays as it is.';
@override
String get loopFormSave => 'Set up the band';
} }
/// The translations for English, as used in the United Kingdom (`en_GB`). /// The translations for English, as used in the United Kingdom (`en_GB`).

View file

@ -12,10 +12,14 @@ import 'package:serverpod_flutter/serverpod_flutter.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Noto Sans' notice (halkyon_ui), the flags' and Unicode CLDR's // Noto Sans' notice (halkyon_ui), the flags' and Unicode CLDR's
// (halkyon_app), and Lucide's for the app's icon. // (halkyon_app), Lucide's for the app's icon, and Polar's where its SDK
// is linked, on Android.
registerFontLicenses(); registerFontLicenses();
registerHalkyonAppLicenses(); registerHalkyonAppLicenses();
registerLucideLicense(); registerLucideLicense();
if (Platform.isAndroid) {
registerPolarLicense();
}
// The instance the build names (`--dart-define=SERVER_URL=…`, or // The instance the build names (`--dart-define=SERVER_URL=…`, or
// `assets/config.json`): the first offered at the first start. // `assets/config.json`): the first offered at the first start.
final builtIn = Instance.parse(await getServerUrl())!; final builtIn = Instance.parse(await getServerUrl())!;

Some files were not shown because too many files have changed in this diff Show more