import 'package:flutter/foundation.dart'; import 'cloud_health/cloud_health_connector.dart'; import 'health_gateway_fallback.dart' if (dart.library.io) 'health_gateway_mobile.dart' as impl; /// Native health ecosystem backing the current device. /// /// Kept in `core` (not the health_sync feature) so the platform layer never /// imports feature code; the biometric controller maps this to the domain's /// `BiometricSource` enum. enum NativeHealthPlatform { /// Apple HealthKit (iOS). appleHealth, /// Google Health Connect (Android). healthConnect, } /// Specific health ecosystem a caller wants to address (Phase 3B contract, /// architecture §4.7.2). [samsungHealth] federates through Health Connect on /// Android; [enterpriseFhir] is the SMART on FHIR cloud connector (provisioning /// pending); [manualFallback] is the always-available desktop/web mode. enum HealthProviderType { appleHealth, healthConnect, samsungHealth, enterpriseFhir, manualFallback, /// Google Health cloud sync (OAuth2 + REST) — the official successor to /// the retired Fitbit Web API (shutdown 2026-10-30); carries Fitbit, /// Pixel and Wear OS data. Wired through the cloud-connector framework /// into the fallback driver for desktop/web. googleHealth, } /// Entitlement / read-permission state reported by /// [IHealthGateway.checkStatus] for a [HealthProviderType]. enum HealthConnectionStatus { /// Provider is supported but no permission decision exists yet. notConfigured, /// The OS health store explicitly refused (or revoked) access. permissionDenied, /// Entitlements + read permissions are granted. connected, /// The provider cannot run on this device/OS (e.g. HealthKit on Windows). unsupportedPlatform, } /// Day-aggregated biometric snapshot returned by /// [IHealthGateway.fetchLatestBiometrics] (architecture §4.7.2). /// /// Every field is nullable: a platform may grant access to weight but not to /// blood glucose (or vice versa), and the payload simply reports what it saw. class HealthDataPayload { const HealthDataPayload({ this.weightLbs, this.activeCaloriesToday, this.bloodGlucoseMgDl, required this.timestamp, required this.source, }); /// Latest body-mass reading of the current calendar day, in pounds. final double? weightLbs; /// Summed active energy expenditure for the current calendar day, in kcal. final double? activeCaloriesToday; /// Latest blood-glucose reading of the current calendar day, in mg/dL /// (mmol/L samples are normalized by the gateway). final double? bloodGlucoseMgDl; /// When the snapshot was assembled. final DateTime timestamp; /// Ecosystem the numbers came from. final HealthProviderType source; } /// Scalar sample returned by the health gateway. /// /// Pure value type: [unit] is a display string (`lb`, `kcal`) — unit /// normalization (kg → lb, joules → kcal) happens inside the gateway so /// consumers only ever deal in pounds and Calories. class HealthMetric { const HealthMetric({ required this.timestamp, required this.value, required this.unit, }); /// When the sample was recorded. final DateTime timestamp; /// Normalized magnitude (pounds for weight, kilocalories for energy). final double value; /// Unit of [value] for display (e.g. `lb`, `kcal`). final String unit; } /// Read-only access to biometrics that were logged locally on this device. /// /// Implemented by the health_sync feature over the encrypted drift cache and /// injected into the fallback gateway so Web/Windows/macOS builds serve /// locally logged data instead of returning nothing. Declared in `core` to /// keep the platform layer free of feature imports (architecture rule 2); /// the dependency points inward: features implement core contracts. abstract class ILocalBiometricReader { /// Weight samples in pounds for `[start, end)`, newest first. Future> readWeightRange({ required DateTime start, required DateTime end, }); /// Active energy burned (kcal) samples for `[start, end)`, newest first. Future> readEnergyRange({ required DateTime start, required DateTime end, }); } /// Cross-platform boundary over the OS health ecosystems (architecture rule /// 2/3: platform-specific packages never leak past this interface). /// /// * **Android / iOS:** implemented by `health_gateway_mobile.dart`, which /// wraps the `health` package (Health Connect / HealthKit) behind /// `kIsWeb` / `defaultTargetPlatform` guards. /// * **Windows / macOS / Web:** implemented by `health_gateway_fallback.dart`, /// a manual-entry mode that never touches platform channels; reads are /// served from the drift-backed cache through an injected /// [ILocalBiometricReader]. abstract class IHealthGateway { /// Whether this device can read/write native health data. `false` on /// Web/Windows/macOS, which signals the UI to offer manual entry. bool get supportsNativeHealth; /// Whether a cloud OAuth partner (Google Health, ...) is wired into this driver /// and can back a sync even without a native health SDK. `false` by /// default; the fallback driver flips it when cloud connectors are /// injected. bool get supportsCloudSync => false; /// Whether any injected cloud partner currently holds a valid grant, i.e. /// a cloud-backed sync would return partner data. `false` by default; /// drivers with connectors override by probing each connector's status. Future isCloudSyncReady() async => false; /// Ecosystem backing [supportsNativeHealth], or `null` in fallback mode. NativeHealthPlatform? get nativePlatform; /// Requests read permission for weight and active energy; returns whether /// access was granted. Always `false` in fallback mode. Future requestPermissions(); /// Weight samples in pounds for `[start, end)`, newest first. /// /// Returns an empty list (never throws) in fallback mode or when the OS /// denies access. Future> fetchWeightHistory({ required DateTime start, required DateTime end, }); /// Active energy burned (kcal) samples for `[start, end)`, newest first. /// /// Returns an empty list (never throws) in fallback mode or when the OS /// denies access. Future> fetchActiveEnergyBurned({ required DateTime start, required DateTime end, }); /// Writes a weight measurement back to the native store; returns whether /// the platform accepted it (always `false` in fallback mode). Future writeWeight({ required double weightLbs, required DateTime timestamp, }); // --------------------------------------------------------------------- // Phase 3B contract (architecture §4.7.2). Default implementations are // derived from the primitives above so hand-written fakes and future // cloud drivers only override what they need. // --------------------------------------------------------------------- /// The [HealthProviderType] this device's driver actually binds to, or /// `null` in fallback mode (Web/Windows/macOS). HealthProviderType? get activeProvider { if (!supportsNativeHealth) return null; final platform = nativePlatform; if (platform == NativeHealthPlatform.appleHealth) { return HealthProviderType.appleHealth; } if (platform == NativeHealthPlatform.healthConnect) { return HealthProviderType.healthConnect; } return null; } /// Verifies current OS entitlement and read permissions for [provider]. /// /// Default: `manualFallback` is always available, `enterpriseFhir` is not /// provisioned yet, native providers map through [activeProvider]. Future checkStatus( HealthProviderType provider) async { if (provider == HealthProviderType.manualFallback) { return HealthConnectionStatus.connected; } if (provider == HealthProviderType.enterpriseFhir) { return HealthConnectionStatus.notConfigured; } final active = activeProvider; if (active == null) { return HealthConnectionStatus.unsupportedPlatform; } return provider == active ? HealthConnectionStatus.notConfigured : HealthConnectionStatus.unsupportedPlatform; } /// Triggers the native OS permission request prompt for [provider]. /// /// Default: manual entry needs no grant; native providers route through /// [requestPermissions]; mismatched providers are refused. Future requestAuthorization(HealthProviderType provider) async { if (provider == HealthProviderType.manualFallback) return true; if (provider != activeProvider) return false; return requestPermissions(); } /// Queries biometrics aggregated across the current calendar day. /// /// Default: composes [fetchWeightHistory] (latest) and /// [fetchActiveEnergyBurned] (sum); drivers may override to add types such /// as blood glucose. Returns `null` when nothing could be read. Future fetchLatestBiometrics() async { final now = DateTime.now(); final start = DateTime(now.year, now.month, now.day); final weights = await fetchWeightHistory(start: start, end: now); final energies = await fetchActiveEnergyBurned(start: start, end: now); if (weights.isEmpty && energies.isEmpty) return null; var energyTotal = 0.0; for (final sample in energies) { energyTotal += sample.value; } return HealthDataPayload( weightLbs: weights.isEmpty ? null : weights.first.value, activeCaloriesToday: energies.isEmpty ? null : energyTotal, timestamp: now, source: activeProvider ?? HealthProviderType.manualFallback, ); } /// Revokes active session bindings or tears down local connections for /// [provider]. Default: no-op (neither HealthKit nor Health Connect /// exposes programmatic revocation to third-party apps — drivers may clear /// cached readiness so the next [checkStatus] re-probes). Future disconnect(HealthProviderType provider) async { debugPrint( 'HealthGateway: disconnect($provider) ignored - revoke access in the ' 'system health settings.', ); } } /// Platform-appropriate factory: native `health`-backed gateway on /// Android/iOS, manual-entry fallback on Web/Windows/macOS. /// /// [localReader] backs fallback reads with the locally logged biometric /// cache; mobile platforms ignore it and query the native store directly. IHealthGateway createHealthGateway({ ILocalBiometricReader? localReader, List cloudConnectors = const [], }) => impl.makeHealthGateway( localReader: localReader, cloudConnectors: cloudConnectors, );