Commit a388346ad0159ecf70fadc7a5498832aaa2c6dd4

Authored by 权海
1 parent bcaf2779

feat(ui):初步添加上传数据的api和添加测试AppleHealth数据页面

Showing 46 changed files with 3042 additions and 162 deletions
@@ -12,6 +12,7 @@ @@ -12,6 +12,7 @@
12 .swiftpm/ 12 .swiftpm/
13 migrate_working_dir/ 13 migrate_working_dir/
14 SDK/ 14 SDK/
  15 +ohos/
15 16
16 # IntelliJ related 17 # IntelliJ related
17 *.iml 18 *.iml
1 package com.doublefeel.app.native 1 package com.doublefeel.app.native
2 2
3 import com.doublefeel.app.MainActivity 3 import com.doublefeel.app.MainActivity
  4 +import com.doublefeel.app.pigeon.HealthActivityTargetData
4 import com.doublefeel.app.pigeon.HealthKitHostApi 5 import com.doublefeel.app.pigeon.HealthKitHostApi
  6 +import com.doublefeel.app.pigeon.HealthSleepUploadDataPoint
  7 +import com.doublefeel.app.pigeon.HealthUploadDataPoint
5 import com.doublefeel.app.pigeon.HealthUploadResult 8 import com.doublefeel.app.pigeon.HealthUploadResult
6 9
7 class HealthKitHostApiImpl( 10 class HealthKitHostApiImpl(
@@ -31,4 +34,34 @@ class HealthKitHostApiImpl( @@ -31,4 +34,34 @@ class HealthKitHostApiImpl(
31 errorMessage = "Native bulk upload runs via Dart HealthKitUploadService; HMS read bridge pending", 34 errorMessage = "Native bulk upload runs via Dart HealthKitUploadService; HMS read bridge pending",
32 ) 35 )
33 } 36 }
  37 +
  38 + override fun fetchHrvData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  39 +
  40 + override fun fetchHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  41 +
  42 + override fun fetchWalkingHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  43 +
  44 + override fun fetchRestingHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  45 +
  46 + override fun fetchSleepingHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  47 +
  48 + override fun fetchOxygenSaturationData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  49 +
  50 + override fun fetchActiveEnergyData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  51 +
  52 + override fun fetchExerciseData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  53 +
  54 + override fun fetchStandData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  55 +
  56 + override fun fetchStepCountData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  57 +
  58 + override fun fetchSleepData(startTime: Long, endTime: Long): List<HealthSleepUploadDataPoint> = emptyList()
  59 +
  60 + override fun fetchSleepingWristTemperatureData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  61 +
  62 + override fun fetchRespiratoryRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  63 +
  64 + override fun fetchIrregularHeartRhythmData(startTime: Long, endTime: Long): List<HealthUploadDataPoint> = emptyList()
  65 +
  66 + override fun fetchActivityTargetData(startTime: Long, endTime: Long): HealthActivityTargetData? = null
34 } 67 }
@@ -112,6 +112,105 @@ data class HealthUploadResult ( @@ -112,6 +112,105 @@ data class HealthUploadResult (
112 112
113 override fun hashCode(): Int = toList().hashCode() 113 override fun hashCode(): Int = toList().hashCode()
114 } 114 }
  115 +
  116 +/** Generated class from Pigeon that represents data sent in messages. */
  117 +data class HealthUploadDataPoint (
  118 + val dataType: Long,
  119 + val time: Long,
  120 + val value: Double
  121 +)
  122 + {
  123 + companion object {
  124 + fun fromList(pigeonVar_list: List<Any?>): HealthUploadDataPoint {
  125 + val dataType = pigeonVar_list[0] as Long
  126 + val time = pigeonVar_list[1] as Long
  127 + val value = pigeonVar_list[2] as Double
  128 + return HealthUploadDataPoint(dataType, time, value)
  129 + }
  130 + }
  131 + fun toList(): List<Any?> {
  132 + return listOf(
  133 + dataType,
  134 + time,
  135 + value,
  136 + )
  137 + }
  138 + override fun equals(other: Any?): Boolean {
  139 + if (other !is HealthUploadDataPoint) {
  140 + return false
  141 + }
  142 + if (this === other) {
  143 + return true
  144 + }
  145 + return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
  146 +
  147 + override fun hashCode(): Int = toList().hashCode()
  148 +}
  149 +
  150 +/** Generated class from Pigeon that represents data sent in messages. */
  151 +data class HealthSleepUploadDataPoint (
  152 + val dataType: Long,
  153 + val fromTime: Long,
  154 + val toTime: Long
  155 +)
  156 + {
  157 + companion object {
  158 + fun fromList(pigeonVar_list: List<Any?>): HealthSleepUploadDataPoint {
  159 + val dataType = pigeonVar_list[0] as Long
  160 + val fromTime = pigeonVar_list[1] as Long
  161 + val toTime = pigeonVar_list[2] as Long
  162 + return HealthSleepUploadDataPoint(dataType, fromTime, toTime)
  163 + }
  164 + }
  165 + fun toList(): List<Any?> {
  166 + return listOf(
  167 + dataType,
  168 + fromTime,
  169 + toTime,
  170 + )
  171 + }
  172 + override fun equals(other: Any?): Boolean {
  173 + if (other !is HealthSleepUploadDataPoint) {
  174 + return false
  175 + }
  176 + if (this === other) {
  177 + return true
  178 + }
  179 + return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
  180 +
  181 + override fun hashCode(): Int = toList().hashCode()
  182 +}
  183 +
  184 +/** Generated class from Pigeon that represents data sent in messages. */
  185 +data class HealthActivityTargetData (
  186 + val move: Long? = null,
  187 + val stand: Long? = null
  188 +)
  189 + {
  190 + companion object {
  191 + fun fromList(pigeonVar_list: List<Any?>): HealthActivityTargetData {
  192 + val move = pigeonVar_list[0] as Long?
  193 + val stand = pigeonVar_list[1] as Long?
  194 + return HealthActivityTargetData(move, stand)
  195 + }
  196 + }
  197 + fun toList(): List<Any?> {
  198 + return listOf(
  199 + move,
  200 + stand,
  201 + )
  202 + }
  203 + override fun equals(other: Any?): Boolean {
  204 + if (other !is HealthActivityTargetData) {
  205 + return false
  206 + }
  207 + if (this === other) {
  208 + return true
  209 + }
  210 + return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
  211 +
  212 + override fun hashCode(): Int = toList().hashCode()
  213 +}
115 private open class HealthKitApiPigeonCodec : StandardMessageCodec() { 214 private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
116 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { 215 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
117 return when (type) { 216 return when (type) {
@@ -120,6 +219,21 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() { @@ -120,6 +219,21 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
120 HealthUploadResult.fromList(it) 219 HealthUploadResult.fromList(it)
121 } 220 }
122 } 221 }
  222 + 130.toByte() -> {
  223 + return (readValue(buffer) as? List<Any?>)?.let {
  224 + HealthUploadDataPoint.fromList(it)
  225 + }
  226 + }
  227 + 131.toByte() -> {
  228 + return (readValue(buffer) as? List<Any?>)?.let {
  229 + HealthSleepUploadDataPoint.fromList(it)
  230 + }
  231 + }
  232 + 132.toByte() -> {
  233 + return (readValue(buffer) as? List<Any?>)?.let {
  234 + HealthActivityTargetData.fromList(it)
  235 + }
  236 + }
123 else -> super.readValueOfType(type, buffer) 237 else -> super.readValueOfType(type, buffer)
124 } 238 }
125 } 239 }
@@ -129,6 +243,18 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() { @@ -129,6 +243,18 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
129 stream.write(129) 243 stream.write(129)
130 writeValue(stream, value.toList()) 244 writeValue(stream, value.toList())
131 } 245 }
  246 + is HealthUploadDataPoint -> {
  247 + stream.write(130)
  248 + writeValue(stream, value.toList())
  249 + }
  250 + is HealthSleepUploadDataPoint -> {
  251 + stream.write(131)
  252 + writeValue(stream, value.toList())
  253 + }
  254 + is HealthActivityTargetData -> {
  255 + stream.write(132)
  256 + writeValue(stream, value.toList())
  257 + }
132 else -> super.writeValue(stream, value) 258 else -> super.writeValue(stream, value)
133 } 259 }
134 } 260 }
@@ -143,6 +269,21 @@ interface HealthKitHostApi { @@ -143,6 +269,21 @@ interface HealthKitHostApi {
143 fun cancelHealthAppAuthorization(): Boolean 269 fun cancelHealthAppAuthorization(): Boolean
144 /** Runs native health read and server upload pipeline. */ 270 /** Runs native health read and server upload pipeline. */
145 fun performHealthUpload(): HealthUploadResult 271 fun performHealthUpload(): HealthUploadResult
  272 + fun fetchHrvData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  273 + fun fetchHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  274 + fun fetchWalkingHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  275 + fun fetchRestingHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  276 + fun fetchSleepingHeartRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  277 + fun fetchOxygenSaturationData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  278 + fun fetchActiveEnergyData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  279 + fun fetchExerciseData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  280 + fun fetchStandData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  281 + fun fetchStepCountData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  282 + fun fetchSleepData(startTime: Long, endTime: Long): List<HealthSleepUploadDataPoint>
  283 + fun fetchSleepingWristTemperatureData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  284 + fun fetchRespiratoryRateData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  285 + fun fetchIrregularHeartRhythmData(startTime: Long, endTime: Long): List<HealthUploadDataPoint>
  286 + fun fetchActivityTargetData(startTime: Long, endTime: Long): HealthActivityTargetData?
146 287
147 companion object { 288 companion object {
148 /** The codec used by HealthKitHostApi. */ 289 /** The codec used by HealthKitHostApi. */
@@ -228,6 +369,276 @@ interface HealthKitHostApi { @@ -228,6 +369,276 @@ interface HealthKitHostApi {
228 channel.setMessageHandler(null) 369 channel.setMessageHandler(null)
229 } 370 }
230 } 371 }
  372 + run {
  373 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHrvData$separatedMessageChannelSuffix", codec)
  374 + if (api != null) {
  375 + channel.setMessageHandler { message, reply ->
  376 + val args = message as List<Any?>
  377 + val startTimeArg = args[0] as Long
  378 + val endTimeArg = args[1] as Long
  379 + val wrapped: List<Any?> = try {
  380 + listOf(api.fetchHrvData(startTimeArg, endTimeArg))
  381 + } catch (exception: Throwable) {
  382 + HealthKitApiPigeonUtils.wrapError(exception)
  383 + }
  384 + reply.reply(wrapped)
  385 + }
  386 + } else {
  387 + channel.setMessageHandler(null)
  388 + }
  389 + }
  390 + run {
  391 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHeartRateData$separatedMessageChannelSuffix", codec)
  392 + if (api != null) {
  393 + channel.setMessageHandler { message, reply ->
  394 + val args = message as List<Any?>
  395 + val startTimeArg = args[0] as Long
  396 + val endTimeArg = args[1] as Long
  397 + val wrapped: List<Any?> = try {
  398 + listOf(api.fetchHeartRateData(startTimeArg, endTimeArg))
  399 + } catch (exception: Throwable) {
  400 + HealthKitApiPigeonUtils.wrapError(exception)
  401 + }
  402 + reply.reply(wrapped)
  403 + }
  404 + } else {
  405 + channel.setMessageHandler(null)
  406 + }
  407 + }
  408 + run {
  409 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchWalkingHeartRateData$separatedMessageChannelSuffix", codec)
  410 + if (api != null) {
  411 + channel.setMessageHandler { message, reply ->
  412 + val args = message as List<Any?>
  413 + val startTimeArg = args[0] as Long
  414 + val endTimeArg = args[1] as Long
  415 + val wrapped: List<Any?> = try {
  416 + listOf(api.fetchWalkingHeartRateData(startTimeArg, endTimeArg))
  417 + } catch (exception: Throwable) {
  418 + HealthKitApiPigeonUtils.wrapError(exception)
  419 + }
  420 + reply.reply(wrapped)
  421 + }
  422 + } else {
  423 + channel.setMessageHandler(null)
  424 + }
  425 + }
  426 + run {
  427 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchRestingHeartRateData$separatedMessageChannelSuffix", codec)
  428 + if (api != null) {
  429 + channel.setMessageHandler { message, reply ->
  430 + val args = message as List<Any?>
  431 + val startTimeArg = args[0] as Long
  432 + val endTimeArg = args[1] as Long
  433 + val wrapped: List<Any?> = try {
  434 + listOf(api.fetchRestingHeartRateData(startTimeArg, endTimeArg))
  435 + } catch (exception: Throwable) {
  436 + HealthKitApiPigeonUtils.wrapError(exception)
  437 + }
  438 + reply.reply(wrapped)
  439 + }
  440 + } else {
  441 + channel.setMessageHandler(null)
  442 + }
  443 + }
  444 + run {
  445 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepingHeartRateData$separatedMessageChannelSuffix", codec)
  446 + if (api != null) {
  447 + channel.setMessageHandler { message, reply ->
  448 + val args = message as List<Any?>
  449 + val startTimeArg = args[0] as Long
  450 + val endTimeArg = args[1] as Long
  451 + val wrapped: List<Any?> = try {
  452 + listOf(api.fetchSleepingHeartRateData(startTimeArg, endTimeArg))
  453 + } catch (exception: Throwable) {
  454 + HealthKitApiPigeonUtils.wrapError(exception)
  455 + }
  456 + reply.reply(wrapped)
  457 + }
  458 + } else {
  459 + channel.setMessageHandler(null)
  460 + }
  461 + }
  462 + run {
  463 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchOxygenSaturationData$separatedMessageChannelSuffix", codec)
  464 + if (api != null) {
  465 + channel.setMessageHandler { message, reply ->
  466 + val args = message as List<Any?>
  467 + val startTimeArg = args[0] as Long
  468 + val endTimeArg = args[1] as Long
  469 + val wrapped: List<Any?> = try {
  470 + listOf(api.fetchOxygenSaturationData(startTimeArg, endTimeArg))
  471 + } catch (exception: Throwable) {
  472 + HealthKitApiPigeonUtils.wrapError(exception)
  473 + }
  474 + reply.reply(wrapped)
  475 + }
  476 + } else {
  477 + channel.setMessageHandler(null)
  478 + }
  479 + }
  480 + run {
  481 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchActiveEnergyData$separatedMessageChannelSuffix", codec)
  482 + if (api != null) {
  483 + channel.setMessageHandler { message, reply ->
  484 + val args = message as List<Any?>
  485 + val startTimeArg = args[0] as Long
  486 + val endTimeArg = args[1] as Long
  487 + val wrapped: List<Any?> = try {
  488 + listOf(api.fetchActiveEnergyData(startTimeArg, endTimeArg))
  489 + } catch (exception: Throwable) {
  490 + HealthKitApiPigeonUtils.wrapError(exception)
  491 + }
  492 + reply.reply(wrapped)
  493 + }
  494 + } else {
  495 + channel.setMessageHandler(null)
  496 + }
  497 + }
  498 + run {
  499 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchExerciseData$separatedMessageChannelSuffix", codec)
  500 + if (api != null) {
  501 + channel.setMessageHandler { message, reply ->
  502 + val args = message as List<Any?>
  503 + val startTimeArg = args[0] as Long
  504 + val endTimeArg = args[1] as Long
  505 + val wrapped: List<Any?> = try {
  506 + listOf(api.fetchExerciseData(startTimeArg, endTimeArg))
  507 + } catch (exception: Throwable) {
  508 + HealthKitApiPigeonUtils.wrapError(exception)
  509 + }
  510 + reply.reply(wrapped)
  511 + }
  512 + } else {
  513 + channel.setMessageHandler(null)
  514 + }
  515 + }
  516 + run {
  517 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchStandData$separatedMessageChannelSuffix", codec)
  518 + if (api != null) {
  519 + channel.setMessageHandler { message, reply ->
  520 + val args = message as List<Any?>
  521 + val startTimeArg = args[0] as Long
  522 + val endTimeArg = args[1] as Long
  523 + val wrapped: List<Any?> = try {
  524 + listOf(api.fetchStandData(startTimeArg, endTimeArg))
  525 + } catch (exception: Throwable) {
  526 + HealthKitApiPigeonUtils.wrapError(exception)
  527 + }
  528 + reply.reply(wrapped)
  529 + }
  530 + } else {
  531 + channel.setMessageHandler(null)
  532 + }
  533 + }
  534 + run {
  535 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchStepCountData$separatedMessageChannelSuffix", codec)
  536 + if (api != null) {
  537 + channel.setMessageHandler { message, reply ->
  538 + val args = message as List<Any?>
  539 + val startTimeArg = args[0] as Long
  540 + val endTimeArg = args[1] as Long
  541 + val wrapped: List<Any?> = try {
  542 + listOf(api.fetchStepCountData(startTimeArg, endTimeArg))
  543 + } catch (exception: Throwable) {
  544 + HealthKitApiPigeonUtils.wrapError(exception)
  545 + }
  546 + reply.reply(wrapped)
  547 + }
  548 + } else {
  549 + channel.setMessageHandler(null)
  550 + }
  551 + }
  552 + run {
  553 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepData$separatedMessageChannelSuffix", codec)
  554 + if (api != null) {
  555 + channel.setMessageHandler { message, reply ->
  556 + val args = message as List<Any?>
  557 + val startTimeArg = args[0] as Long
  558 + val endTimeArg = args[1] as Long
  559 + val wrapped: List<Any?> = try {
  560 + listOf(api.fetchSleepData(startTimeArg, endTimeArg))
  561 + } catch (exception: Throwable) {
  562 + HealthKitApiPigeonUtils.wrapError(exception)
  563 + }
  564 + reply.reply(wrapped)
  565 + }
  566 + } else {
  567 + channel.setMessageHandler(null)
  568 + }
  569 + }
  570 + run {
  571 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepingWristTemperatureData$separatedMessageChannelSuffix", codec)
  572 + if (api != null) {
  573 + channel.setMessageHandler { message, reply ->
  574 + val args = message as List<Any?>
  575 + val startTimeArg = args[0] as Long
  576 + val endTimeArg = args[1] as Long
  577 + val wrapped: List<Any?> = try {
  578 + listOf(api.fetchSleepingWristTemperatureData(startTimeArg, endTimeArg))
  579 + } catch (exception: Throwable) {
  580 + HealthKitApiPigeonUtils.wrapError(exception)
  581 + }
  582 + reply.reply(wrapped)
  583 + }
  584 + } else {
  585 + channel.setMessageHandler(null)
  586 + }
  587 + }
  588 + run {
  589 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchRespiratoryRateData$separatedMessageChannelSuffix", codec)
  590 + if (api != null) {
  591 + channel.setMessageHandler { message, reply ->
  592 + val args = message as List<Any?>
  593 + val startTimeArg = args[0] as Long
  594 + val endTimeArg = args[1] as Long
  595 + val wrapped: List<Any?> = try {
  596 + listOf(api.fetchRespiratoryRateData(startTimeArg, endTimeArg))
  597 + } catch (exception: Throwable) {
  598 + HealthKitApiPigeonUtils.wrapError(exception)
  599 + }
  600 + reply.reply(wrapped)
  601 + }
  602 + } else {
  603 + channel.setMessageHandler(null)
  604 + }
  605 + }
  606 + run {
  607 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchIrregularHeartRhythmData$separatedMessageChannelSuffix", codec)
  608 + if (api != null) {
  609 + channel.setMessageHandler { message, reply ->
  610 + val args = message as List<Any?>
  611 + val startTimeArg = args[0] as Long
  612 + val endTimeArg = args[1] as Long
  613 + val wrapped: List<Any?> = try {
  614 + listOf(api.fetchIrregularHeartRhythmData(startTimeArg, endTimeArg))
  615 + } catch (exception: Throwable) {
  616 + HealthKitApiPigeonUtils.wrapError(exception)
  617 + }
  618 + reply.reply(wrapped)
  619 + }
  620 + } else {
  621 + channel.setMessageHandler(null)
  622 + }
  623 + }
  624 + run {
  625 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchActivityTargetData$separatedMessageChannelSuffix", codec)
  626 + if (api != null) {
  627 + channel.setMessageHandler { message, reply ->
  628 + val args = message as List<Any?>
  629 + val startTimeArg = args[0] as Long
  630 + val endTimeArg = args[1] as Long
  631 + val wrapped: List<Any?> = try {
  632 + listOf(api.fetchActivityTargetData(startTimeArg, endTimeArg))
  633 + } catch (exception: Throwable) {
  634 + HealthKitApiPigeonUtils.wrapError(exception)
  635 + }
  636 + reply.reply(wrapped)
  637 + }
  638 + } else {
  639 + channel.setMessageHandler(null)
  640 + }
  641 + }
231 } 642 }
232 } 643 }
233 } 644 }
@@ -141,6 +141,11 @@ interface WearEngineHostApi { @@ -141,6 +141,11 @@ interface WearEngineHostApi {
141 fun registerMessageReceiver(): Boolean 141 fun registerMessageReceiver(): Boolean
142 fun sendTextMessage(message: String): Boolean 142 fun sendTextMessage(message: String): Boolean
143 fun sendWatchSyncPayload(jsonPayload: String): Boolean 143 fun sendWatchSyncPayload(jsonPayload: String): Boolean
  144 + /**
  145 + * Opens the system photo picker and returns a local PNG file path after
  146 + * removing the image background on the host platform.
  147 + */
  148 + fun pickImageAndRemoveBackground(): String?
144 149
145 companion object { 150 companion object {
146 /** The codec used by WearEngineHostApi. */ 151 /** The codec used by WearEngineHostApi. */
@@ -230,6 +235,21 @@ interface WearEngineHostApi { @@ -230,6 +235,21 @@ interface WearEngineHostApi {
230 channel.setMessageHandler(null) 235 channel.setMessageHandler(null)
231 } 236 }
232 } 237 }
  238 + run {
  239 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.pickImageAndRemoveBackground$separatedMessageChannelSuffix", codec)
  240 + if (api != null) {
  241 + channel.setMessageHandler { _, reply ->
  242 + val wrapped: List<Any?> = try {
  243 + listOf(api.pickImageAndRemoveBackground())
  244 + } catch (exception: Throwable) {
  245 + WearEngineApiPigeonUtils.wrapError(exception)
  246 + }
  247 + reply.reply(wrapped)
  248 + }
  249 + } else {
  250 + channel.setMessageHandler(null)
  251 + }
  252 + }
233 } 253 }
234 } 254 }
235 } 255 }
1 import SwiftUI 1 import SwiftUI
2 -import UIKit  
3 2
4 struct DFDefaultWatchHomeView: View { 3 struct DFDefaultWatchHomeView: View {
5 - @State private var batteryLevel: Float = UIDevice.current.batteryLevel  
6 - @State private var batteryState: UIDevice.BatteryState = UIDevice.current.batteryState  
7 -  
8 @State private var weekday: String = "" 4 @State private var weekday: String = ""
9 @State private var day: String = "" 5 @State private var day: String = ""
10 6
11 var hasData: Bool = false 7 var hasData: Bool = false
12 var hrvValue: String = "38ms" 8 var hrvValue: String = "38ms"
13 var hrvTime: String = "19:04" 9 var hrvTime: String = "19:04"
14 -  
15 - var batteryColor: Color{  
16 - if batteryLevel < 0.1{  
17 - // 0  
18 - return Color(hex: "#FF0000")  
19 - }else if batteryLevel < 40{  
20 - // 25  
21 - return Color(hex: "#FF9A6E")  
22 - }else if batteryLevel < 60{  
23 - // 50  
24 - return Color(hex: "#7B9BFB")  
25 - }else if batteryLevel < 80{  
26 - // 75  
27 - return Color(hex: "#3BD49D")  
28 - }else{  
29 - // 100  
30 - return Color(hex: "#3BD49D")  
31 - }  
32 - }  
33 - var batteryImageName: String{  
34 - if batteryLevel < 0.1{  
35 - // 0  
36 - return "battery.0percent"  
37 - }else if batteryLevel < 40{  
38 - // 25  
39 - return "battery.25percent"  
40 - }else if batteryLevel < 60{  
41 - // 50  
42 - return "battery.50percent"  
43 - }else if batteryLevel < 80{  
44 - // 75  
45 - return "battery.75percent"  
46 - }else{  
47 - // 100  
48 - return "battery.100percent"  
49 - }  
50 - }  
51 10
52 var body: some View { 11 var body: some View {
53 VStack{ 12 VStack{
54 HStack{ 13 HStack{
55 - Image(systemName: batteryImageName)  
56 - .frame(height: 20)  
57 - .tint(batteryColor)  
58 Spacer() 14 Spacer()
59 Text(weekday) 15 Text(weekday)
60 .font(.system(size: 16, weight: .medium)) 16 .font(.system(size: 16, weight: .medium))
@@ -98,30 +54,9 @@ struct DFDefaultWatchHomeView: View { @@ -98,30 +54,9 @@ struct DFDefaultWatchHomeView: View {
98 .padding(.all, 15) 54 .padding(.all, 15)
99 .onAppear { 55 .onAppear {
100 updateDate() 56 updateDate()
101 - updateBattery()  
102 - NotificationCenter.default.addObserver(  
103 - forName: UIDevice.batteryLevelDidChangeNotification,  
104 - object: nil,  
105 - queue: .main  
106 - ) { _ in  
107 - updateBattery()  
108 - }  
109 -  
110 - NotificationCenter.default.addObserver(  
111 - forName: UIDevice.batteryStateDidChangeNotification,  
112 - object: nil,  
113 - queue: .main  
114 - ) { _ in  
115 - updateBattery()  
116 - }  
117 } 57 }
118 } 58 }
119 -  
120 - private func updateBattery() {  
121 - UIDevice.current.isBatteryMonitoringEnabled = true  
122 - batteryLevel = UIDevice.current.batteryLevel  
123 - batteryState = UIDevice.current.batteryState  
124 - } 59 +
125 private func updateDate(){ 60 private func updateDate(){
126 let calendar = Calendar.current 61 let calendar = Calendar.current
127 let component = calendar.component(.weekday, from: .now) 62 let component = calendar.component(.weekday, from: .now)
@@ -3,15 +3,15 @@ @@ -3,15 +3,15 @@
3 archiveVersion = 1; 3 archiveVersion = 1;
4 classes = { 4 classes = {
5 }; 5 };
6 - objectVersion = 77; 6 + objectVersion = 54;
7 objects = { 7 objects = {
8 8
9 /* Begin PBXBuildFile section */ 9 /* Begin PBXBuildFile section */
10 0798E27DB95F2A823881D75A /* Pods_Runner_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A488EFC0BD642ED604C4E43 /* Pods_Runner_Watch_App.framework */; }; 10 0798E27DB95F2A823881D75A /* Pods_Runner_Watch_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A488EFC0BD642ED604C4E43 /* Pods_Runner_Watch_App.framework */; };
11 20A3C4994C9B0F60D1F3BCBE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8DC48B3736B7FEB548D0E65 /* Pods_Runner.framework */; }; 11 20A3C4994C9B0F60D1F3BCBE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E8DC48B3736B7FEB548D0E65 /* Pods_Runner.framework */; };
12 - 66FB19002FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19032FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift */; };  
13 - 66FB19012FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19042FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift */; };  
14 - 66FB19022FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19052FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift */; }; 12 + 66FB19002FDBC80100F515B4 /* DFWatchFigmaTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19032FDBC80100F515B4 /* DFWatchFigmaTokens.swift */; };
  13 + 66FB19012FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19042FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift */; };
  14 + 66FB19022FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FB19052FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift */; };
15 66FBE58C2FDA4F0F00F515B4 /* Runner Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 15 66FBE58C2FDA4F0F00F515B4 /* Runner Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
16 /* End PBXBuildFile section */ 16 /* End PBXBuildFile section */
17 17
@@ -54,9 +54,9 @@ @@ -54,9 +54,9 @@
54 27E4D5A8995AB1EC91E7330E /* Pods-Runner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App.debug.xcconfig"; sourceTree = "<group>"; }; 54 27E4D5A8995AB1EC91E7330E /* Pods-Runner Watch App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner Watch App.debug.xcconfig"; path = "Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App.debug.xcconfig"; sourceTree = "<group>"; };
55 4A9B7C102FDB0A1200F515B4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; }; 55 4A9B7C102FDB0A1200F515B4 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
56 4A9B7C112FDB0A1200F515B4 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; }; 56 4A9B7C112FDB0A1200F515B4 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
57 - 66FB19032FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift"; sourceTree = "<group>"; };  
58 - 66FB19042FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift"; sourceTree = "<group>"; };  
59 - 66FB19052FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift"; sourceTree = "<group>"; }; 57 + 66FB19032FDBC80100F515B4 /* DFWatchFigmaTokens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift"; sourceTree = "<group>"; };
  58 + 66FB19042FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift"; sourceTree = "<group>"; };
  59 + 66FB19052FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift"; sourceTree = "<group>"; };
60 66FBE57E2FDA4F0E00F515B4 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 60 66FBE57E2FDA4F0E00F515B4 /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
61 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Runner Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 61 66FBE58B2FDA4F0F00F515B4 /* Runner Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Runner Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
62 66FBE5A72FDA518C00F515B4 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 62 66FBE5A72FDA518C00F515B4 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
@@ -97,6 +97,8 @@ @@ -97,6 +97,8 @@
97 }; 97 };
98 66FBE58F2FDA4F0F00F515B4 /* Runner Watch App */ = { 98 66FBE58F2FDA4F0F00F515B4 /* Runner Watch App */ = {
99 isa = PBXFileSystemSynchronizedRootGroup; 99 isa = PBXFileSystemSynchronizedRootGroup;
  100 + exceptions = (
  101 + );
100 path = "Runner Watch App"; 102 path = "Runner Watch App";
101 sourceTree = "<group>"; 103 sourceTree = "<group>";
102 }; 104 };
@@ -134,9 +136,9 @@ @@ -134,9 +136,9 @@
134 66FB19072FDBC80100F515B4 /* FigmaHome Preview Sources */ = { 136 66FB19072FDBC80100F515B4 /* FigmaHome Preview Sources */ = {
135 isa = PBXGroup; 137 isa = PBXGroup;
136 children = ( 138 children = (
137 - 66FB19052FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift */,  
138 - 66FB19042FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift */,  
139 - 66FB19032FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift */, 139 + 66FB19052FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift */,
  140 + 66FB19042FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift */,
  141 + 66FB19032FDBC80100F515B4 /* DFWatchFigmaTokens.swift */,
140 ); 142 );
141 name = "FigmaHome Preview Sources"; 143 name = "FigmaHome Preview Sources";
142 sourceTree = "<group>"; 144 sourceTree = "<group>";
@@ -303,14 +305,10 @@ @@ -303,14 +305,10 @@
303 inputFileListPaths = ( 305 inputFileListPaths = (
304 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", 306 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
305 ); 307 );
306 - inputPaths = (  
307 - );  
308 name = "[CP] Copy Pods Resources"; 308 name = "[CP] Copy Pods Resources";
309 outputFileListPaths = ( 309 outputFileListPaths = (
310 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", 310 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
311 ); 311 );
312 - outputPaths = (  
313 - );  
314 runOnlyForDeploymentPostprocessing = 0; 312 runOnlyForDeploymentPostprocessing = 0;
315 shellPath = /bin/sh; 313 shellPath = /bin/sh;
316 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; 314 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -381,14 +379,10 @@ @@ -381,14 +379,10 @@
381 inputFileListPaths = ( 379 inputFileListPaths = (
382 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 380 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
383 ); 381 );
384 - inputPaths = (  
385 - );  
386 name = "[CP] Embed Pods Frameworks"; 382 name = "[CP] Embed Pods Frameworks";
387 outputFileListPaths = ( 383 outputFileListPaths = (
388 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 384 "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
389 ); 385 );
390 - outputPaths = (  
391 - );  
392 runOnlyForDeploymentPostprocessing = 0; 386 runOnlyForDeploymentPostprocessing = 0;
393 shellPath = /bin/sh; 387 shellPath = /bin/sh;
394 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 388 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -402,14 +396,10 @@ @@ -402,14 +396,10 @@
402 inputFileListPaths = ( 396 inputFileListPaths = (
403 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist", 397 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-input-files.xcfilelist",
404 ); 398 );
405 - inputPaths = (  
406 - );  
407 name = "[CP] Embed Pods Frameworks"; 399 name = "[CP] Embed Pods Frameworks";
408 outputFileListPaths = ( 400 outputFileListPaths = (
409 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist", 401 "${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks-${CONFIGURATION}-output-files.xcfilelist",
410 ); 402 );
411 - outputPaths = (  
412 - );  
413 runOnlyForDeploymentPostprocessing = 0; 403 runOnlyForDeploymentPostprocessing = 0;
414 shellPath = /bin/sh; 404 shellPath = /bin/sh;
415 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks.sh\"\n"; 405 shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner Watch App/Pods-Runner Watch App-frameworks.sh\"\n";
@@ -451,9 +441,9 @@ @@ -451,9 +441,9 @@
451 isa = PBXSourcesBuildPhase; 441 isa = PBXSourcesBuildPhase;
452 buildActionMask = 2147483647; 442 buildActionMask = 2147483647;
453 files = ( 443 files = (
454 - 66FB19022FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaDetailViews.swift in Sources */,  
455 - 66FB19012FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaHomeViews.swift in Sources */,  
456 - 66FB19002FDBC80100F515B4 /* Runner Watch App/FigmaHome/DFWatchFigmaTokens.swift in Sources */, 444 + 66FB19022FDBC80100F515B4 /* DFWatchFigmaDetailViews.swift in Sources */,
  445 + 66FB19012FDBC80100F515B4 /* DFWatchFigmaHomeViews.swift in Sources */,
  446 + 66FB19002FDBC80100F515B4 /* DFWatchFigmaTokens.swift in Sources */,
457 ); 447 );
458 runOnlyForDeploymentPostprocessing = 0; 448 runOnlyForDeploymentPostprocessing = 0;
459 }; 449 };
@@ -133,6 +133,170 @@ final class HealthDataReader { @@ -133,6 +133,170 @@ final class HealthDataReader {
133 } 133 }
134 } 134 }
135 135
  136 + func fetchHrvData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  137 + try await fetchQuantitySamples(
  138 + identifier: .heartRateVariabilitySDNN,
  139 + dataType: .hrv,
  140 + unit: .secondUnit(with: .milli),
  141 + startDate: startDate,
  142 + endDate: endDate
  143 + )
  144 + }
  145 +
  146 + func fetchHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  147 + try await fetchQuantitySamples(
  148 + identifier: .heartRate,
  149 + dataType: .heartRate,
  150 + unit: HKUnit.count().unitDivided(by: .minute()),
  151 + startDate: startDate,
  152 + endDate: endDate
  153 + )
  154 + }
  155 +
  156 + func fetchWalkingHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  157 + try await fetchQuantitySamples(
  158 + identifier: .walkingHeartRateAverage,
  159 + dataType: .walkingHeartRate,
  160 + unit: HKUnit.count().unitDivided(by: .minute()),
  161 + startDate: startDate,
  162 + endDate: endDate
  163 + )
  164 + }
  165 +
  166 + func fetchRestingHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  167 + try await fetchQuantitySamples(
  168 + identifier: .restingHeartRate,
  169 + dataType: .restingHeartRate,
  170 + unit: HKUnit.count().unitDivided(by: .minute()),
  171 + startDate: startDate,
  172 + endDate: endDate
  173 + )
  174 + }
  175 +
  176 + func fetchSleepingHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  177 + let sleepIntervals = try await fetchSleepIntervals(startDate: startDate, endDate: endDate)
  178 + var points: [NativeHealthDataPoint] = []
  179 + for interval in sleepIntervals where NativeSleepStage.isAsleep(interval.dataType) {
  180 + let samples = try await fetchQuantitySamples(
  181 + identifier: .heartRate,
  182 + dataType: .sleepingHeartRate,
  183 + unit: HKUnit.count().unitDivided(by: .minute()),
  184 + startDate: Date(timeIntervalSince1970: interval.fromTime),
  185 + endDate: Date(timeIntervalSince1970: interval.toTime)
  186 + )
  187 + points.append(contentsOf: samples)
  188 + }
  189 + return points
  190 + }
  191 +
  192 + func fetchOxygenSaturationData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  193 + let points = try await fetchQuantitySamples(
  194 + identifier: .oxygenSaturation,
  195 + dataType: .oxygenSaturation,
  196 + unit: .percent(),
  197 + startDate: startDate,
  198 + endDate: endDate
  199 + )
  200 + return points.map { point in
  201 + NativeHealthDataPoint(dataType: point.dataType, time: point.time, value: point.value * 100)
  202 + }
  203 + }
  204 +
  205 + func fetchActiveEnergyData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  206 + try await fetchQuantitySamples(
  207 + identifier: .activeEnergyBurned,
  208 + dataType: .activeEnergy,
  209 + unit: .kilocalorie(),
  210 + startDate: startDate,
  211 + endDate: endDate
  212 + )
  213 + }
  214 +
  215 + func fetchExerciseData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  216 + try await fetchQuantitySamples(
  217 + identifier: .appleExerciseTime,
  218 + dataType: .exercise,
  219 + unit: .minute(),
  220 + startDate: startDate,
  221 + endDate: endDate
  222 + )
  223 + }
  224 +
  225 + func fetchStandData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  226 + try await fetchQuantitySamples(
  227 + identifier: .appleStandTime,
  228 + dataType: .stand,
  229 + unit: .minute(),
  230 + startDate: startDate,
  231 + endDate: endDate
  232 + )
  233 + }
  234 +
  235 + func fetchStepCountData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  236 + try await fetchDailyCumulativeSamples(
  237 + identifier: .stepCount,
  238 + dataType: .steps,
  239 + unit: .count(),
  240 + startDate: startDate,
  241 + endDate: endDate
  242 + )
  243 + }
  244 +
  245 + func fetchSleepData(startDate: Date, endDate: Date) async throws -> [NativeSleepInterval] {
  246 + try await fetchSleepIntervals(startDate: startDate, endDate: endDate)
  247 + }
  248 +
  249 + func fetchSleepingWristTemperatureData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  250 + try await fetchQuantitySamples(
  251 + identifier: .appleSleepingWristTemperature,
  252 + dataType: .sleepingWristTemperature,
  253 + unit: .degreeCelsius(),
  254 + startDate: startDate,
  255 + endDate: endDate
  256 + )
  257 + }
  258 +
  259 + func fetchRespiratoryRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  260 + try await fetchQuantitySamples(
  261 + identifier: .respiratoryRate,
  262 + dataType: .respiratoryRate,
  263 + unit: HKUnit.count().unitDivided(by: .minute()),
  264 + startDate: startDate,
  265 + endDate: endDate
  266 + )
  267 + }
  268 +
  269 + func fetchIrregularHeartRhythmData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  270 + try await fetchIrregularHeartRhythmEvents(startDate: startDate, endDate: endDate)
  271 + }
  272 +
  273 + func fetchActivityTargetData(startDate: Date, endDate: Date) async throws -> NativeActivityTarget? {
  274 + let calendar = Calendar.current
  275 + let start = calendar.dateComponents([.era, .year, .month, .day], from: startDate)
  276 + let end = calendar.dateComponents([.era, .year, .month, .day], from: endDate)
  277 + let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: start, end: end)
  278 +
  279 + return try await withCheckedThrowingContinuation { continuation in
  280 + let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, error in
  281 + if let error {
  282 + continuation.resume(throwing: error)
  283 + return
  284 + }
  285 + guard let summary = summaries?.last else {
  286 + continuation.resume(returning: nil)
  287 + return
  288 + }
  289 + continuation.resume(
  290 + returning: NativeActivityTarget(
  291 + move: Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())),
  292 + stand: Int(summary.appleStandHoursGoal.doubleValue(for: .count()))
  293 + )
  294 + )
  295 + }
  296 + healthStore.execute(query)
  297 + }
  298 + }
  299 +
136 private func fetchHeartRateFamily(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { 300 private func fetchHeartRateFamily(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
137 async let heartRate = fetchQuantitySamples( 301 async let heartRate = fetchQuantitySamples(
138 identifier: .heartRate, 302 identifier: .heartRate,
@@ -108,6 +108,66 @@ final class HealthKitService { @@ -108,6 +108,66 @@ final class HealthKitService {
108 } 108 }
109 } 109 }
110 110
  111 + func fetchHrvData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  112 + try await reader.fetchHrvData(startDate: startDate, endDate: endDate)
  113 + }
  114 +
  115 + func fetchHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  116 + try await reader.fetchHeartRateData(startDate: startDate, endDate: endDate)
  117 + }
  118 +
  119 + func fetchWalkingHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  120 + try await reader.fetchWalkingHeartRateData(startDate: startDate, endDate: endDate)
  121 + }
  122 +
  123 + func fetchRestingHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  124 + try await reader.fetchRestingHeartRateData(startDate: startDate, endDate: endDate)
  125 + }
  126 +
  127 + func fetchSleepingHeartRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  128 + try await reader.fetchSleepingHeartRateData(startDate: startDate, endDate: endDate)
  129 + }
  130 +
  131 + func fetchOxygenSaturationData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  132 + try await reader.fetchOxygenSaturationData(startDate: startDate, endDate: endDate)
  133 + }
  134 +
  135 + func fetchActiveEnergyData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  136 + try await reader.fetchActiveEnergyData(startDate: startDate, endDate: endDate)
  137 + }
  138 +
  139 + func fetchExerciseData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  140 + try await reader.fetchExerciseData(startDate: startDate, endDate: endDate)
  141 + }
  142 +
  143 + func fetchStandData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  144 + try await reader.fetchStandData(startDate: startDate, endDate: endDate)
  145 + }
  146 +
  147 + func fetchStepCountData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  148 + try await reader.fetchStepCountData(startDate: startDate, endDate: endDate)
  149 + }
  150 +
  151 + func fetchSleepData(startDate: Date, endDate: Date) async throws -> [NativeSleepInterval] {
  152 + try await reader.fetchSleepData(startDate: startDate, endDate: endDate)
  153 + }
  154 +
  155 + func fetchSleepingWristTemperatureData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  156 + try await reader.fetchSleepingWristTemperatureData(startDate: startDate, endDate: endDate)
  157 + }
  158 +
  159 + func fetchRespiratoryRateData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  160 + try await reader.fetchRespiratoryRateData(startDate: startDate, endDate: endDate)
  161 + }
  162 +
  163 + func fetchIrregularHeartRhythmData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  164 + try await reader.fetchIrregularHeartRhythmData(startDate: startDate, endDate: endDate)
  165 + }
  166 +
  167 + func fetchActivityTargetData(startDate: Date, endDate: Date) async throws -> NativeActivityTarget? {
  168 + try await reader.fetchActivityTargetData(startDate: startDate, endDate: endDate)
  169 + }
  170 +
111 private func earliestStartDate() -> Date { 171 private func earliestStartDate() -> Date {
112 NativeHealthDataType.allCases 172 NativeHealthDataType.allCases
113 .filter { $0 != .unknown } 173 .filter { $0 != .unknown }
@@ -174,4 +234,3 @@ private extension NativeHealthDataType { @@ -174,4 +234,3 @@ private extension NativeHealthDataType {
174 } 234 }
175 } 235 }
176 } 236 }
177 -  
@@ -49,6 +49,25 @@ struct NativeSleepInterval: Codable { @@ -49,6 +49,25 @@ struct NativeSleepInterval: Codable {
49 let toTime: TimeInterval 49 let toTime: TimeInterval
50 } 50 }
51 51
  52 +struct NativeActivityTarget {
  53 + let move: Int?
  54 + let stand: Int?
  55 +}
  56 +
  57 +enum NativeSleepStage {
  58 + static func isAsleep(_ rawValue: Int) -> Bool {
  59 + guard let value = HKCategoryValueSleepAnalysis(rawValue: rawValue) else {
  60 + return false
  61 + }
  62 + switch value {
  63 + case .asleepUnspecified, .asleepCore, .asleepDeep, .asleepREM:
  64 + return true
  65 + default:
  66 + return false
  67 + }
  68 + }
  69 +}
  70 +
52 struct NativeHealthSyncSummary { 71 struct NativeHealthSyncSummary {
53 var commonCount = 0 72 var commonCount = 0
54 var sleepCount = 0 73 var sleepCount = 0
@@ -145,11 +145,112 @@ struct HealthUploadResult: Hashable { @@ -145,11 +145,112 @@ struct HealthUploadResult: Hashable {
145 } 145 }
146 } 146 }
147 147
  148 +/// Generated class from Pigeon that represents data sent in messages.
  149 +struct HealthUploadDataPoint: Hashable {
  150 + var dataType: Int64
  151 + var time: Int64
  152 + var value: Double
  153 +
  154 +
  155 + // swift-format-ignore: AlwaysUseLowerCamelCase
  156 + static func fromList(_ pigeonVar_list: [Any?]) -> HealthUploadDataPoint? {
  157 + let dataType = pigeonVar_list[0] as! Int64
  158 + let time = pigeonVar_list[1] as! Int64
  159 + let value = pigeonVar_list[2] as! Double
  160 +
  161 + return HealthUploadDataPoint(
  162 + dataType: dataType,
  163 + time: time,
  164 + value: value
  165 + )
  166 + }
  167 + func toList() -> [Any?] {
  168 + return [
  169 + dataType,
  170 + time,
  171 + value,
  172 + ]
  173 + }
  174 + static func == (lhs: HealthUploadDataPoint, rhs: HealthUploadDataPoint) -> Bool {
  175 + return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
  176 + func hash(into hasher: inout Hasher) {
  177 + deepHashHealthKitApi(value: toList(), hasher: &hasher)
  178 + }
  179 +}
  180 +
  181 +/// Generated class from Pigeon that represents data sent in messages.
  182 +struct HealthSleepUploadDataPoint: Hashable {
  183 + var dataType: Int64
  184 + var fromTime: Int64
  185 + var toTime: Int64
  186 +
  187 +
  188 + // swift-format-ignore: AlwaysUseLowerCamelCase
  189 + static func fromList(_ pigeonVar_list: [Any?]) -> HealthSleepUploadDataPoint? {
  190 + let dataType = pigeonVar_list[0] as! Int64
  191 + let fromTime = pigeonVar_list[1] as! Int64
  192 + let toTime = pigeonVar_list[2] as! Int64
  193 +
  194 + return HealthSleepUploadDataPoint(
  195 + dataType: dataType,
  196 + fromTime: fromTime,
  197 + toTime: toTime
  198 + )
  199 + }
  200 + func toList() -> [Any?] {
  201 + return [
  202 + dataType,
  203 + fromTime,
  204 + toTime,
  205 + ]
  206 + }
  207 + static func == (lhs: HealthSleepUploadDataPoint, rhs: HealthSleepUploadDataPoint) -> Bool {
  208 + return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
  209 + func hash(into hasher: inout Hasher) {
  210 + deepHashHealthKitApi(value: toList(), hasher: &hasher)
  211 + }
  212 +}
  213 +
  214 +/// Generated class from Pigeon that represents data sent in messages.
  215 +struct HealthActivityTargetData: Hashable {
  216 + var move: Int64? = nil
  217 + var stand: Int64? = nil
  218 +
  219 +
  220 + // swift-format-ignore: AlwaysUseLowerCamelCase
  221 + static func fromList(_ pigeonVar_list: [Any?]) -> HealthActivityTargetData? {
  222 + let move: Int64? = nilOrValue(pigeonVar_list[0])
  223 + let stand: Int64? = nilOrValue(pigeonVar_list[1])
  224 +
  225 + return HealthActivityTargetData(
  226 + move: move,
  227 + stand: stand
  228 + )
  229 + }
  230 + func toList() -> [Any?] {
  231 + return [
  232 + move,
  233 + stand,
  234 + ]
  235 + }
  236 + static func == (lhs: HealthActivityTargetData, rhs: HealthActivityTargetData) -> Bool {
  237 + return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
  238 + func hash(into hasher: inout Hasher) {
  239 + deepHashHealthKitApi(value: toList(), hasher: &hasher)
  240 + }
  241 +}
  242 +
148 private class HealthKitApiPigeonCodecReader: FlutterStandardReader { 243 private class HealthKitApiPigeonCodecReader: FlutterStandardReader {
149 override func readValue(ofType type: UInt8) -> Any? { 244 override func readValue(ofType type: UInt8) -> Any? {
150 switch type { 245 switch type {
151 case 129: 246 case 129:
152 return HealthUploadResult.fromList(self.readValue() as! [Any?]) 247 return HealthUploadResult.fromList(self.readValue() as! [Any?])
  248 + case 130:
  249 + return HealthUploadDataPoint.fromList(self.readValue() as! [Any?])
  250 + case 131:
  251 + return HealthSleepUploadDataPoint.fromList(self.readValue() as! [Any?])
  252 + case 132:
  253 + return HealthActivityTargetData.fromList(self.readValue() as! [Any?])
153 default: 254 default:
154 return super.readValue(ofType: type) 255 return super.readValue(ofType: type)
155 } 256 }
@@ -161,6 +262,15 @@ private class HealthKitApiPigeonCodecWriter: FlutterStandardWriter { @@ -161,6 +262,15 @@ private class HealthKitApiPigeonCodecWriter: FlutterStandardWriter {
161 if let value = value as? HealthUploadResult { 262 if let value = value as? HealthUploadResult {
162 super.writeByte(129) 263 super.writeByte(129)
163 super.writeValue(value.toList()) 264 super.writeValue(value.toList())
  265 + } else if let value = value as? HealthUploadDataPoint {
  266 + super.writeByte(130)
  267 + super.writeValue(value.toList())
  268 + } else if let value = value as? HealthSleepUploadDataPoint {
  269 + super.writeByte(131)
  270 + super.writeValue(value.toList())
  271 + } else if let value = value as? HealthActivityTargetData {
  272 + super.writeByte(132)
  273 + super.writeValue(value.toList())
164 } else { 274 } else {
165 super.writeValue(value) 275 super.writeValue(value)
166 } 276 }
@@ -190,6 +300,21 @@ protocol HealthKitHostApi { @@ -190,6 +300,21 @@ protocol HealthKitHostApi {
190 func cancelHealthAppAuthorization() throws -> Bool 300 func cancelHealthAppAuthorization() throws -> Bool
191 /// Runs native health read and server upload pipeline. 301 /// Runs native health read and server upload pipeline.
192 func performHealthUpload() throws -> HealthUploadResult 302 func performHealthUpload() throws -> HealthUploadResult
  303 + func fetchHrvData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  304 + func fetchHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  305 + func fetchWalkingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  306 + func fetchRestingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  307 + func fetchSleepingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  308 + func fetchOxygenSaturationData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  309 + func fetchActiveEnergyData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  310 + func fetchExerciseData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  311 + func fetchStandData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  312 + func fetchStepCountData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  313 + func fetchSleepData(startTime: Int64, endTime: Int64) throws -> [HealthSleepUploadDataPoint]
  314 + func fetchSleepingWristTemperatureData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  315 + func fetchRespiratoryRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  316 + func fetchIrregularHeartRhythmData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint]
  317 + func fetchActivityTargetData(startTime: Int64, endTime: Int64) throws -> HealthActivityTargetData?
193 } 318 }
194 319
195 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. 320 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -265,5 +390,245 @@ class HealthKitHostApiSetup { @@ -265,5 +390,245 @@ class HealthKitHostApiSetup {
265 } else { 390 } else {
266 performHealthUploadChannel.setMessageHandler(nil) 391 performHealthUploadChannel.setMessageHandler(nil)
267 } 392 }
  393 + let fetchHrvDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHrvData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  394 + if let api = api {
  395 + fetchHrvDataChannel.setMessageHandler { message, reply in
  396 + let args = message as! [Any?]
  397 + let startTimeArg = args[0] as! Int64
  398 + let endTimeArg = args[1] as! Int64
  399 + do {
  400 + let result = try api.fetchHrvData(startTime: startTimeArg, endTime: endTimeArg)
  401 + reply(wrapResult(result))
  402 + } catch {
  403 + reply(wrapError(error))
  404 + }
  405 + }
  406 + } else {
  407 + fetchHrvDataChannel.setMessageHandler(nil)
  408 + }
  409 + let fetchHeartRateDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHeartRateData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  410 + if let api = api {
  411 + fetchHeartRateDataChannel.setMessageHandler { message, reply in
  412 + let args = message as! [Any?]
  413 + let startTimeArg = args[0] as! Int64
  414 + let endTimeArg = args[1] as! Int64
  415 + do {
  416 + let result = try api.fetchHeartRateData(startTime: startTimeArg, endTime: endTimeArg)
  417 + reply(wrapResult(result))
  418 + } catch {
  419 + reply(wrapError(error))
  420 + }
  421 + }
  422 + } else {
  423 + fetchHeartRateDataChannel.setMessageHandler(nil)
  424 + }
  425 + let fetchWalkingHeartRateDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchWalkingHeartRateData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  426 + if let api = api {
  427 + fetchWalkingHeartRateDataChannel.setMessageHandler { message, reply in
  428 + let args = message as! [Any?]
  429 + let startTimeArg = args[0] as! Int64
  430 + let endTimeArg = args[1] as! Int64
  431 + do {
  432 + let result = try api.fetchWalkingHeartRateData(startTime: startTimeArg, endTime: endTimeArg)
  433 + reply(wrapResult(result))
  434 + } catch {
  435 + reply(wrapError(error))
  436 + }
  437 + }
  438 + } else {
  439 + fetchWalkingHeartRateDataChannel.setMessageHandler(nil)
  440 + }
  441 + let fetchRestingHeartRateDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchRestingHeartRateData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  442 + if let api = api {
  443 + fetchRestingHeartRateDataChannel.setMessageHandler { message, reply in
  444 + let args = message as! [Any?]
  445 + let startTimeArg = args[0] as! Int64
  446 + let endTimeArg = args[1] as! Int64
  447 + do {
  448 + let result = try api.fetchRestingHeartRateData(startTime: startTimeArg, endTime: endTimeArg)
  449 + reply(wrapResult(result))
  450 + } catch {
  451 + reply(wrapError(error))
  452 + }
  453 + }
  454 + } else {
  455 + fetchRestingHeartRateDataChannel.setMessageHandler(nil)
  456 + }
  457 + let fetchSleepingHeartRateDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepingHeartRateData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  458 + if let api = api {
  459 + fetchSleepingHeartRateDataChannel.setMessageHandler { message, reply in
  460 + let args = message as! [Any?]
  461 + let startTimeArg = args[0] as! Int64
  462 + let endTimeArg = args[1] as! Int64
  463 + do {
  464 + let result = try api.fetchSleepingHeartRateData(startTime: startTimeArg, endTime: endTimeArg)
  465 + reply(wrapResult(result))
  466 + } catch {
  467 + reply(wrapError(error))
  468 + }
  469 + }
  470 + } else {
  471 + fetchSleepingHeartRateDataChannel.setMessageHandler(nil)
  472 + }
  473 + let fetchOxygenSaturationDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchOxygenSaturationData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  474 + if let api = api {
  475 + fetchOxygenSaturationDataChannel.setMessageHandler { message, reply in
  476 + let args = message as! [Any?]
  477 + let startTimeArg = args[0] as! Int64
  478 + let endTimeArg = args[1] as! Int64
  479 + do {
  480 + let result = try api.fetchOxygenSaturationData(startTime: startTimeArg, endTime: endTimeArg)
  481 + reply(wrapResult(result))
  482 + } catch {
  483 + reply(wrapError(error))
  484 + }
  485 + }
  486 + } else {
  487 + fetchOxygenSaturationDataChannel.setMessageHandler(nil)
  488 + }
  489 + let fetchActiveEnergyDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchActiveEnergyData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  490 + if let api = api {
  491 + fetchActiveEnergyDataChannel.setMessageHandler { message, reply in
  492 + let args = message as! [Any?]
  493 + let startTimeArg = args[0] as! Int64
  494 + let endTimeArg = args[1] as! Int64
  495 + do {
  496 + let result = try api.fetchActiveEnergyData(startTime: startTimeArg, endTime: endTimeArg)
  497 + reply(wrapResult(result))
  498 + } catch {
  499 + reply(wrapError(error))
  500 + }
  501 + }
  502 + } else {
  503 + fetchActiveEnergyDataChannel.setMessageHandler(nil)
  504 + }
  505 + let fetchExerciseDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchExerciseData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  506 + if let api = api {
  507 + fetchExerciseDataChannel.setMessageHandler { message, reply in
  508 + let args = message as! [Any?]
  509 + let startTimeArg = args[0] as! Int64
  510 + let endTimeArg = args[1] as! Int64
  511 + do {
  512 + let result = try api.fetchExerciseData(startTime: startTimeArg, endTime: endTimeArg)
  513 + reply(wrapResult(result))
  514 + } catch {
  515 + reply(wrapError(error))
  516 + }
  517 + }
  518 + } else {
  519 + fetchExerciseDataChannel.setMessageHandler(nil)
  520 + }
  521 + let fetchStandDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchStandData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  522 + if let api = api {
  523 + fetchStandDataChannel.setMessageHandler { message, reply in
  524 + let args = message as! [Any?]
  525 + let startTimeArg = args[0] as! Int64
  526 + let endTimeArg = args[1] as! Int64
  527 + do {
  528 + let result = try api.fetchStandData(startTime: startTimeArg, endTime: endTimeArg)
  529 + reply(wrapResult(result))
  530 + } catch {
  531 + reply(wrapError(error))
  532 + }
  533 + }
  534 + } else {
  535 + fetchStandDataChannel.setMessageHandler(nil)
  536 + }
  537 + let fetchStepCountDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchStepCountData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  538 + if let api = api {
  539 + fetchStepCountDataChannel.setMessageHandler { message, reply in
  540 + let args = message as! [Any?]
  541 + let startTimeArg = args[0] as! Int64
  542 + let endTimeArg = args[1] as! Int64
  543 + do {
  544 + let result = try api.fetchStepCountData(startTime: startTimeArg, endTime: endTimeArg)
  545 + reply(wrapResult(result))
  546 + } catch {
  547 + reply(wrapError(error))
  548 + }
  549 + }
  550 + } else {
  551 + fetchStepCountDataChannel.setMessageHandler(nil)
  552 + }
  553 + let fetchSleepDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  554 + if let api = api {
  555 + fetchSleepDataChannel.setMessageHandler { message, reply in
  556 + let args = message as! [Any?]
  557 + let startTimeArg = args[0] as! Int64
  558 + let endTimeArg = args[1] as! Int64
  559 + do {
  560 + let result = try api.fetchSleepData(startTime: startTimeArg, endTime: endTimeArg)
  561 + reply(wrapResult(result))
  562 + } catch {
  563 + reply(wrapError(error))
  564 + }
  565 + }
  566 + } else {
  567 + fetchSleepDataChannel.setMessageHandler(nil)
  568 + }
  569 + let fetchSleepingWristTemperatureDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepingWristTemperatureData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  570 + if let api = api {
  571 + fetchSleepingWristTemperatureDataChannel.setMessageHandler { message, reply in
  572 + let args = message as! [Any?]
  573 + let startTimeArg = args[0] as! Int64
  574 + let endTimeArg = args[1] as! Int64
  575 + do {
  576 + let result = try api.fetchSleepingWristTemperatureData(startTime: startTimeArg, endTime: endTimeArg)
  577 + reply(wrapResult(result))
  578 + } catch {
  579 + reply(wrapError(error))
  580 + }
  581 + }
  582 + } else {
  583 + fetchSleepingWristTemperatureDataChannel.setMessageHandler(nil)
  584 + }
  585 + let fetchRespiratoryRateDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchRespiratoryRateData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  586 + if let api = api {
  587 + fetchRespiratoryRateDataChannel.setMessageHandler { message, reply in
  588 + let args = message as! [Any?]
  589 + let startTimeArg = args[0] as! Int64
  590 + let endTimeArg = args[1] as! Int64
  591 + do {
  592 + let result = try api.fetchRespiratoryRateData(startTime: startTimeArg, endTime: endTimeArg)
  593 + reply(wrapResult(result))
  594 + } catch {
  595 + reply(wrapError(error))
  596 + }
  597 + }
  598 + } else {
  599 + fetchRespiratoryRateDataChannel.setMessageHandler(nil)
  600 + }
  601 + let fetchIrregularHeartRhythmDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchIrregularHeartRhythmData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  602 + if let api = api {
  603 + fetchIrregularHeartRhythmDataChannel.setMessageHandler { message, reply in
  604 + let args = message as! [Any?]
  605 + let startTimeArg = args[0] as! Int64
  606 + let endTimeArg = args[1] as! Int64
  607 + do {
  608 + let result = try api.fetchIrregularHeartRhythmData(startTime: startTimeArg, endTime: endTimeArg)
  609 + reply(wrapResult(result))
  610 + } catch {
  611 + reply(wrapError(error))
  612 + }
  613 + }
  614 + } else {
  615 + fetchIrregularHeartRhythmDataChannel.setMessageHandler(nil)
  616 + }
  617 + let fetchActivityTargetDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchActivityTargetData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  618 + if let api = api {
  619 + fetchActivityTargetDataChannel.setMessageHandler { message, reply in
  620 + let args = message as! [Any?]
  621 + let startTimeArg = args[0] as! Int64
  622 + let endTimeArg = args[1] as! Int64
  623 + do {
  624 + let result = try api.fetchActivityTargetData(startTime: startTimeArg, endTime: endTimeArg)
  625 + reply(wrapResult(result))
  626 + } catch {
  627 + reply(wrapError(error))
  628 + }
  629 + }
  630 + } else {
  631 + fetchActivityTargetDataChannel.setMessageHandler(nil)
  632 + }
268 } 633 }
269 } 634 }
@@ -50,6 +50,109 @@ final class HealthKitHostApiImpl: HealthKitHostApi { @@ -50,6 +50,109 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
50 ) 50 )
51 } 51 }
52 52
  53 + func fetchHrvData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  54 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchHrvData)
  55 + }
  56 +
  57 + func fetchHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  58 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchHeartRateData)
  59 + }
  60 +
  61 + func fetchWalkingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  62 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchWalkingHeartRateData)
  63 + }
  64 +
  65 + func fetchRestingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  66 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchRestingHeartRateData)
  67 + }
  68 +
  69 + func fetchSleepingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  70 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchSleepingHeartRateData)
  71 + }
  72 +
  73 + func fetchOxygenSaturationData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  74 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchOxygenSaturationData)
  75 + }
  76 +
  77 + func fetchActiveEnergyData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  78 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchActiveEnergyData)
  79 + }
  80 +
  81 + func fetchExerciseData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  82 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchExerciseData)
  83 + }
  84 +
  85 + func fetchStandData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  86 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchStandData)
  87 + }
  88 +
  89 + func fetchStepCountData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  90 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchStepCountData)
  91 + }
  92 +
  93 + func fetchSleepData(startTime: Int64, endTime: Int64) throws -> [HealthSleepUploadDataPoint] {
  94 + let range = makeDateRange(startTime: startTime, endTime: endTime)
  95 + let intervals = try runBlockingThrows {
  96 + try await self.service.fetchSleepData(startDate: range.startDate, endDate: range.endDate)
  97 + }
  98 + return intervals.map { interval in
  99 + HealthSleepUploadDataPoint(
  100 + dataType: Int64(interval.dataType),
  101 + fromTime: Int64(interval.fromTime.rounded()),
  102 + toTime: Int64(interval.toTime.rounded())
  103 + )
  104 + }
  105 + }
  106 +
  107 + func fetchSleepingWristTemperatureData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  108 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchSleepingWristTemperatureData)
  109 + }
  110 +
  111 + func fetchRespiratoryRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  112 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchRespiratoryRateData)
  113 + }
  114 +
  115 + func fetchIrregularHeartRhythmData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
  116 + try fetchCommon(startTime: startTime, endTime: endTime, service.fetchIrregularHeartRhythmData)
  117 + }
  118 +
  119 + func fetchActivityTargetData(startTime: Int64, endTime: Int64) throws -> HealthActivityTargetData? {
  120 + let range = makeDateRange(startTime: startTime, endTime: endTime)
  121 + let target = try runBlockingThrows {
  122 + try await self.service.fetchActivityTargetData(startDate: range.startDate, endDate: range.endDate)
  123 + }
  124 + guard let target else { return nil }
  125 + return HealthActivityTargetData(
  126 + move: target.move.map(Int64.init),
  127 + stand: target.stand.map(Int64.init)
  128 + )
  129 + }
  130 +
  131 + private func fetchCommon(
  132 + startTime: Int64,
  133 + endTime: Int64,
  134 + _ fetch: @escaping (Date, Date) async throws -> [NativeHealthDataPoint]
  135 + ) throws -> [HealthUploadDataPoint] {
  136 + let range = makeDateRange(startTime: startTime, endTime: endTime)
  137 + let points = try runBlockingThrows {
  138 + try await fetch(range.startDate, range.endDate)
  139 + }
  140 + return points.map { point in
  141 + HealthUploadDataPoint(
  142 + dataType: Int64(point.dataType.rawValue),
  143 + time: Int64(point.time.rounded()),
  144 + value: point.value
  145 + )
  146 + }
  147 + }
  148 +
  149 + private func makeDateRange(startTime: Int64, endTime: Int64) -> (startDate: Date, endDate: Date) {
  150 + (
  151 + Date(timeIntervalSince1970: TimeInterval(startTime)),
  152 + Date(timeIntervalSince1970: TimeInterval(endTime))
  153 + )
  154 + }
  155 +
53 private func runAuthorizationRequest() -> (success: Bool, error: Error?) { 156 private func runAuthorizationRequest() -> (success: Bool, error: Error?) {
54 var result: (Bool, Error?) = (false, nil) 157 var result: (Bool, Error?) = (false, nil)
55 let semaphore = DispatchSemaphore(value: 0) 158 let semaphore = DispatchSemaphore(value: 0)
@@ -73,6 +176,21 @@ private func runBlocking<T>(_ operation: @escaping () async -> T) -> T { @@ -73,6 +176,21 @@ private func runBlocking<T>(_ operation: @escaping () async -> T) -> T {
73 return result! 176 return result!
74 } 177 }
75 178
  179 +private func runBlockingThrows<T>(_ operation: @escaping () async throws -> T) throws -> T {
  180 + let semaphore = DispatchSemaphore(value: 0)
  181 + var result: Result<T, Error>?
  182 + Task {
  183 + do {
  184 + result = .success(try await operation())
  185 + } catch {
  186 + result = .failure(error)
  187 + }
  188 + semaphore.signal()
  189 + }
  190 + waitForSemaphore(semaphore)
  191 + return try result!.get()
  192 +}
  193 +
76 private func waitForSemaphore(_ semaphore: DispatchSemaphore) { 194 private func waitForSemaphore(_ semaphore: DispatchSemaphore) {
77 if Thread.isMainThread { 195 if Thread.isMainThread {
78 while semaphore.wait(timeout: .now() + 0.05) == .timedOut { 196 while semaphore.wait(timeout: .now() + 0.05) == .timedOut {
@@ -188,6 +188,8 @@ protocol WearEngineHostApi { @@ -188,6 +188,8 @@ protocol WearEngineHostApi {
188 func registerMessageReceiver() throws -> Bool 188 func registerMessageReceiver() throws -> Bool
189 func sendTextMessage(message: String) throws -> Bool 189 func sendTextMessage(message: String) throws -> Bool
190 func sendWatchSyncPayload(jsonPayload: String) throws -> Bool 190 func sendWatchSyncPayload(jsonPayload: String) throws -> Bool
  191 + /// Opens the system photo picker and returns a local PNG file path after
  192 + /// removing the image background on the host platform.
191 func pickImageAndRemoveBackground() throws -> String? 193 func pickImageAndRemoveBackground() throws -> String?
192 } 194 }
193 195
@@ -266,6 +268,8 @@ class WearEngineHostApiSetup { @@ -266,6 +268,8 @@ class WearEngineHostApiSetup {
266 } else { 268 } else {
267 sendWatchSyncPayloadChannel.setMessageHandler(nil) 269 sendWatchSyncPayloadChannel.setMessageHandler(nil)
268 } 270 }
  271 + /// Opens the system photo picker and returns a local PNG file path after
  272 + /// removing the image background on the host platform.
269 let pickImageAndRemoveBackgroundChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.pickImageAndRemoveBackground\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) 273 let pickImageAndRemoveBackgroundChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.pickImageAndRemoveBackground\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
270 if let api = api { 274 if let api = api {
271 pickImageAndRemoveBackgroundChannel.setMessageHandler { _, reply in 275 pickImageAndRemoveBackgroundChannel.setMessageHandler { _, reply in
  1 +import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
  2 +import 'package:doublefeel_flutter/core/network/api_paths.dart';
  3 +import 'package:doublefeel_flutter/core/network/dio_client.dart';
  4 +import 'package:doublefeel_flutter/core/result/app_result.dart';
  5 +import 'package:doublefeel_flutter/core/result/safe_call.dart';
  6 +
  7 +import 'models/apple_health_upload_record.dart';
  8 +import 'models/apple_health_upload_request.dart';
  9 +import 'models/upload_activity_target.dart';
  10 +
  11 +class AppleHealthUploadApi {
  12 + AppleHealthUploadApi(this._dioClient);
  13 +
  14 + final DioClient _dioClient;
  15 +
  16 + Future<AppResult<void>> uploadCommonData(
  17 + AppleHealthCommonUploadRequest request,
  18 + ) {
  19 + return safeCall(
  20 + call: () async {
  21 + await _dioClient.dio.post(
  22 + ApiPaths.healthUploadCommon,
  23 + data: request.toJson(),
  24 + );
  25 + },
  26 + );
  27 + }
  28 +
  29 + Future<AppResult<AppleHealthLatestUploadRecordList>>
  30 + getLatestCommonUploadRecordList({
  31 + HttpErrorHandlingPolicy? errorHandlingPolicy =
  32 + HttpErrorHandlingPolicy.defaultPolicy,
  33 + }) {
  34 + return safeCall(
  35 + call: () async {
  36 + final response = await _dioClient.dio.get(ApiPaths.healthUploadCommon);
  37 + return AppleHealthLatestUploadRecordList.fromJson(
  38 + response.data as Map<String, dynamic>,
  39 + );
  40 + },
  41 + errorHandlingPolicy: errorHandlingPolicy,
  42 + );
  43 + }
  44 +
  45 + Future<AppResult<void>> uploadSleepData(
  46 + AppleHealthSleepUploadRequest request,
  47 + ) {
  48 + return safeCall(
  49 + call: () async {
  50 + await _dioClient.dio.post(
  51 + ApiPaths.healthUploadSleep,
  52 + data: request.toJson(),
  53 + );
  54 + },
  55 + );
  56 + }
  57 +
  58 + Future<AppResult<AppleHealthLatestSleepUploadRecord>>
  59 + getLatestSleepUploadRecord({
  60 + HttpErrorHandlingPolicy? errorHandlingPolicy =
  61 + HttpErrorHandlingPolicy.defaultPolicy,
  62 + }) {
  63 + return safeCall(
  64 + call: () async {
  65 + final response = await _dioClient.dio.get(ApiPaths.healthUploadSleep);
  66 + return AppleHealthLatestSleepUploadRecord.fromJson(
  67 + response.data as Map<String, dynamic>,
  68 + );
  69 + },
  70 + errorHandlingPolicy: errorHandlingPolicy,
  71 + );
  72 + }
  73 +
  74 + Future<AppResult<void>> uploadActivityTarget(
  75 + HealthActivityTargetUploadData target,
  76 + ) {
  77 + return safeCall(
  78 + call: () async {
  79 + await _dioClient.dio.post(
  80 + ApiPaths.healthActivityTarget,
  81 + data: target.toJson(),
  82 + );
  83 + },
  84 + );
  85 + }
  86 +}
  1 +import 'package:doublefeel_flutter/core/result/app_result.dart';
  2 +
  3 +import 'apple_health_upload_api.dart';
  4 +import 'models/apple_health_upload_record.dart';
  5 +import 'models/apple_health_upload_request.dart';
  6 +import 'models/apple_health_upload_sample.dart';
  7 +import 'models/upload_activity_target.dart';
  8 +import 'models/upload_sleep.dart';
  9 +
  10 +class AppleHealthUploadTool {
  11 + AppleHealthUploadTool(this._api);
  12 +
  13 + final AppleHealthUploadApi _api;
  14 +
  15 + Future<AppleHealthUploadResult> upload({
  16 + List<AppleHealthUploadSample> commonData = const [],
  17 + List<HealthSleepUploadData> sleepData = const [],
  18 + HealthActivityTargetUploadData? activityTarget,
  19 + }) async {
  20 + AppResult<void>? commonResult;
  21 + AppResult<void>? sleepResult;
  22 + AppResult<void>? activityTargetResult;
  23 +
  24 + if (commonData.isNotEmpty) {
  25 + commonResult = await _api.uploadCommonData(
  26 + AppleHealthCommonUploadRequest(dataList: commonData),
  27 + );
  28 + }
  29 +
  30 + if (sleepData.isNotEmpty) {
  31 + sleepResult = await _api.uploadSleepData(
  32 + AppleHealthSleepUploadRequest(dataList: sleepData),
  33 + );
  34 + }
  35 +
  36 + if (activityTarget != null) {
  37 + activityTargetResult = await _api.uploadActivityTarget(activityTarget);
  38 + }
  39 +
  40 + return AppleHealthUploadResult(
  41 + commonUploadSuccess:
  42 + commonResult == null || commonResult is AppSuccess<void>,
  43 + sleepUploadSuccess:
  44 + sleepResult == null || sleepResult is AppSuccess<void>,
  45 + activityTargetUploadSuccess: activityTargetResult == null ||
  46 + activityTargetResult is AppSuccess<void>,
  47 + );
  48 + }
  49 +
  50 + Future<AppResult<AppleHealthLatestUploadRecordList>>
  51 + getLatestCommonUploadRecordList() {
  52 + return _api.getLatestCommonUploadRecordList();
  53 + }
  54 +
  55 + Future<AppResult<AppleHealthLatestSleepUploadRecord>>
  56 + getLatestSleepUploadRecord() {
  57 + return _api.getLatestSleepUploadRecord();
  58 + }
  59 +}
  60 +
  61 +class AppleHealthUploadResult {
  62 + const AppleHealthUploadResult({
  63 + required this.commonUploadSuccess,
  64 + required this.sleepUploadSuccess,
  65 + required this.activityTargetUploadSuccess,
  66 + });
  67 +
  68 + final bool commonUploadSuccess;
  69 + final bool sleepUploadSuccess;
  70 + final bool activityTargetUploadSuccess;
  71 +
  72 + bool get isSuccess =>
  73 + commonUploadSuccess && sleepUploadSuccess && activityTargetUploadSuccess;
  74 +}
  1 +import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
  2 +
  3 +import 'models/apple_health_upload_sample.dart';
  4 +import 'models/health_upload_data_type.dart';
  5 +import 'models/upload_active_energy.dart';
  6 +import 'models/upload_exercise.dart';
  7 +import 'models/upload_heart_rate.dart';
  8 +import 'models/upload_hrv.dart';
  9 +import 'models/upload_irregular_heart_rhythm.dart';
  10 +import 'models/upload_oxygen_saturation.dart';
  11 +import 'models/upload_respiratory_rate.dart';
  12 +import 'models/upload_resting_heart_rate.dart';
  13 +import 'models/upload_sleep.dart';
  14 +import 'models/upload_sleeping_heart_rate.dart';
  15 +import 'models/upload_sleeping_wrist_temperature.dart';
  16 +import 'models/upload_stand.dart';
  17 +import 'models/upload_step_count.dart';
  18 +import 'models/upload_walking_heart_rate.dart';
  19 +
  20 +extension HealthUploadDataPointMapper on HealthUploadDataPoint {
  21 + AppleHealthUploadSample? toAppleHealthUploadSample() {
  22 + switch (HealthUploadDataType.fromValue(dataType)) {
  23 + case HealthUploadDataType.hrv:
  24 + return HealthHrvUploadData(time: time, value: value);
  25 + case HealthUploadDataType.heartRate:
  26 + return HealthHeartRateUploadData(time: time, value: value);
  27 + case HealthUploadDataType.oxygenSaturation:
  28 + return HealthOxygenSaturationUploadData(time: time, value: value);
  29 + case HealthUploadDataType.activeEnergy:
  30 + return HealthActiveEnergyUploadData(time: time, value: value);
  31 + case HealthUploadDataType.exercise:
  32 + return HealthExerciseUploadData(time: time, value: value);
  33 + case HealthUploadDataType.stand:
  34 + return HealthStandUploadData(time: time, value: value);
  35 + case HealthUploadDataType.steps:
  36 + return HealthStepCountUploadData(time: time, value: value);
  37 + case HealthUploadDataType.walkingHeartRate:
  38 + return HealthWalkingHeartRateUploadData(time: time, value: value);
  39 + case HealthUploadDataType.restingHeartRate:
  40 + return HealthRestingHeartRateUploadData(time: time, value: value);
  41 + case HealthUploadDataType.sleepingHeartRate:
  42 + return HealthSleepingHeartRateUploadData(time: time, value: value);
  43 + case HealthUploadDataType.sleepingWristTemperature:
  44 + return HealthSleepingWristTemperatureUploadData(
  45 + time: time,
  46 + value: value,
  47 + );
  48 + case HealthUploadDataType.respiratoryRate:
  49 + return HealthRespiratoryRateUploadData(time: time, value: value);
  50 + case HealthUploadDataType.irregularHeartRhythm:
  51 + return HealthIrregularHeartRhythmUploadData(time: time, value: value);
  52 + case HealthUploadDataType.unknown:
  53 + case HealthUploadDataType.sleep:
  54 + return null;
  55 + }
  56 + }
  57 +}
  58 +
  59 +extension HealthSleepUploadDataPointMapper on HealthSleepUploadDataPoint {
  60 + HealthSleepUploadData toHealthSleepUploadData() {
  61 + return HealthSleepUploadData(
  62 + dataType: dataType,
  63 + fromTime: fromTime,
  64 + toTime: toTime,
  65 + );
  66 + }
  67 +}
  1 +import 'health_upload_data_type.dart';
  2 +
  3 +class AppleHealthLatestUploadRecord {
  4 + const AppleHealthLatestUploadRecord({
  5 + this.dataType,
  6 + this.latestDataTime,
  7 + });
  8 +
  9 + final HealthUploadDataType? dataType;
  10 + final int? latestDataTime;
  11 +
  12 + factory AppleHealthLatestUploadRecord.fromJson(Map<String, dynamic> json) {
  13 + final rawDataType = json['data_type'];
  14 + return AppleHealthLatestUploadRecord(
  15 + dataType: rawDataType is int
  16 + ? HealthUploadDataType.fromValue(rawDataType)
  17 + : null,
  18 + latestDataTime: json['latest_data_time'] as int?,
  19 + );
  20 + }
  21 +}
  22 +
  23 +class AppleHealthLatestUploadRecordList {
  24 + const AppleHealthLatestUploadRecordList({this.latestDataTimeList});
  25 +
  26 + final List<AppleHealthLatestUploadRecord>? latestDataTimeList;
  27 +
  28 + factory AppleHealthLatestUploadRecordList.fromJson(
  29 + Map<String, dynamic> json,
  30 + ) {
  31 + return AppleHealthLatestUploadRecordList(
  32 + latestDataTimeList: (json['latest_data_time_list'] as List<dynamic>?)
  33 + ?.map((item) => AppleHealthLatestUploadRecord.fromJson(
  34 + item as Map<String, dynamic>,
  35 + ))
  36 + .toList(),
  37 + );
  38 + }
  39 +}
  40 +
  41 +class AppleHealthLatestSleepUploadRecord {
  42 + const AppleHealthLatestSleepUploadRecord({this.latestDataTime});
  43 +
  44 + final int? latestDataTime;
  45 +
  46 + factory AppleHealthLatestSleepUploadRecord.fromJson(
  47 + Map<String, dynamic> json,
  48 + ) {
  49 + return AppleHealthLatestSleepUploadRecord(
  50 + latestDataTime: json['latest_data_time'] as int?,
  51 + );
  52 + }
  53 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'upload_sleep.dart';
  3 +
  4 +class AppleHealthCommonUploadRequest {
  5 + const AppleHealthCommonUploadRequest({required this.dataList});
  6 +
  7 + final List<AppleHealthUploadSample> dataList;
  8 +
  9 + Map<String, dynamic> toJson() {
  10 + return {
  11 + 'data_list': dataList.map((item) => item.toJson()).toList(),
  12 + };
  13 + }
  14 +}
  15 +
  16 +class AppleHealthSleepUploadRequest {
  17 + const AppleHealthSleepUploadRequest({required this.dataList});
  18 +
  19 + final List<HealthSleepUploadData> dataList;
  20 +
  21 + Map<String, dynamic> toJson() {
  22 + return {
  23 + 'data_list': dataList.map((item) => item.toJson()).toList(),
  24 + };
  25 + }
  26 +}
  1 +import 'health_upload_data_type.dart';
  2 +
  3 +abstract class AppleHealthUploadSample {
  4 + const AppleHealthUploadSample({
  5 + required this.dataType,
  6 + required this.time,
  7 + required this.value,
  8 + });
  9 +
  10 + final HealthUploadDataType dataType;
  11 + final int time;
  12 + final double value;
  13 +
  14 + Map<String, dynamic> toJson() => {
  15 + 'data_type': dataType.value,
  16 + 'time': time,
  17 + 'value': value,
  18 + };
  19 +}
  20 +
  21 +int healthUploadTimeFromJson(Object? value) {
  22 + if (value is int) return value;
  23 + if (value is double) return value.round();
  24 + if (value is String) return int.tryParse(value) ?? 0;
  25 + return 0;
  26 +}
  27 +
  28 +double healthUploadDoubleFromJson(Object? value) {
  29 + if (value is num) return value.toDouble();
  30 + if (value is String) return double.tryParse(value) ?? 0;
  31 + return 0;
  32 +}
  1 +enum HealthUploadDataType {
  2 + unknown(0),
  3 + hrv(1),
  4 + heartRate(2),
  5 + oxygenSaturation(3),
  6 + activeEnergy(4),
  7 + exercise(5),
  8 + stand(6),
  9 + steps(7),
  10 + walkingHeartRate(8),
  11 + restingHeartRate(9),
  12 + sleepingHeartRate(10),
  13 + sleepingWristTemperature(11),
  14 + respiratoryRate(12),
  15 + irregularHeartRhythm(13),
  16 + sleep(100);
  17 +
  18 + const HealthUploadDataType(this.value);
  19 +
  20 + final int value;
  21 +
  22 + static HealthUploadDataType fromValue(int value) {
  23 + return HealthUploadDataType.values.firstWhere(
  24 + (type) => type.value == value,
  25 + orElse: () => HealthUploadDataType.unknown,
  26 + );
  27 + }
  28 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthActiveEnergyUploadData extends AppleHealthUploadSample {
  5 + const HealthActiveEnergyUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.activeEnergy);
  9 +
  10 + factory HealthActiveEnergyUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthActiveEnergyUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +class HealthActivityTargetUploadData {
  2 + const HealthActivityTargetUploadData({
  3 + this.move,
  4 + this.stand,
  5 + });
  6 +
  7 + final int? move;
  8 + final int? stand;
  9 +
  10 + factory HealthActivityTargetUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthActivityTargetUploadData(
  12 + move: json['move'] as int?,
  13 + stand: json['stand'] as int?,
  14 + );
  15 + }
  16 +
  17 + Map<String, dynamic> toJson() {
  18 + return {
  19 + if (move != null) 'move': move,
  20 + if (stand != null) 'stand': stand,
  21 + };
  22 + }
  23 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthExerciseUploadData extends AppleHealthUploadSample {
  5 + const HealthExerciseUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.exercise);
  9 +
  10 + factory HealthExerciseUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthExerciseUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthHeartRateUploadData extends AppleHealthUploadSample {
  5 + const HealthHeartRateUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.heartRate);
  9 +
  10 + factory HealthHeartRateUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthHeartRateUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthHrvUploadData extends AppleHealthUploadSample {
  5 + const HealthHrvUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.hrv);
  9 +
  10 + factory HealthHrvUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthHrvUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthIrregularHeartRhythmUploadData extends AppleHealthUploadSample {
  5 + const HealthIrregularHeartRhythmUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.irregularHeartRhythm);
  9 +
  10 + factory HealthIrregularHeartRhythmUploadData.fromJson(
  11 + Map<String, dynamic> json,
  12 + ) {
  13 + return HealthIrregularHeartRhythmUploadData(
  14 + time: healthUploadTimeFromJson(json['time']),
  15 + value: healthUploadDoubleFromJson(json['value']),
  16 + );
  17 + }
  18 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthOxygenSaturationUploadData extends AppleHealthUploadSample {
  5 + const HealthOxygenSaturationUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.oxygenSaturation);
  9 +
  10 + factory HealthOxygenSaturationUploadData.fromJson(
  11 + Map<String, dynamic> json,
  12 + ) {
  13 + return HealthOxygenSaturationUploadData(
  14 + time: healthUploadTimeFromJson(json['time']),
  15 + value: healthUploadDoubleFromJson(json['value']),
  16 + );
  17 + }
  18 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthRespiratoryRateUploadData extends AppleHealthUploadSample {
  5 + const HealthRespiratoryRateUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.respiratoryRate);
  9 +
  10 + factory HealthRespiratoryRateUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthRespiratoryRateUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthRestingHeartRateUploadData extends AppleHealthUploadSample {
  5 + const HealthRestingHeartRateUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.restingHeartRate);
  9 +
  10 + factory HealthRestingHeartRateUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthRestingHeartRateUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +
  3 +class HealthSleepUploadData {
  4 + const HealthSleepUploadData({
  5 + required this.dataType,
  6 + required this.fromTime,
  7 + required this.toTime,
  8 + });
  9 +
  10 + final int dataType;
  11 + final int fromTime;
  12 + final int toTime;
  13 +
  14 + Map<String, dynamic> toJson() {
  15 + return {
  16 + 'data_type': dataType,
  17 + 'from_time': fromTime,
  18 + 'to_time': toTime,
  19 + };
  20 + }
  21 +
  22 + factory HealthSleepUploadData.fromJson(Map<String, dynamic> json) {
  23 + return HealthSleepUploadData(
  24 + dataType: healthUploadTimeFromJson(json['data_type']),
  25 + fromTime: healthUploadTimeFromJson(json['from_time']),
  26 + toTime: healthUploadTimeFromJson(json['to_time']),
  27 + );
  28 + }
  29 +
  30 + @override
  31 + bool operator ==(Object other) {
  32 + if (identical(this, other)) return true;
  33 + return other is HealthSleepUploadData &&
  34 + other.fromTime == fromTime &&
  35 + other.toTime == toTime &&
  36 + other.dataType == dataType;
  37 + }
  38 +
  39 + @override
  40 + int get hashCode => Object.hash(dataType, fromTime, toTime);
  41 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthSleepingHeartRateUploadData extends AppleHealthUploadSample {
  5 + const HealthSleepingHeartRateUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.sleepingHeartRate);
  9 +
  10 + factory HealthSleepingHeartRateUploadData.fromJson(
  11 + Map<String, dynamic> json,
  12 + ) {
  13 + return HealthSleepingHeartRateUploadData(
  14 + time: healthUploadTimeFromJson(json['time']),
  15 + value: healthUploadDoubleFromJson(json['value']),
  16 + );
  17 + }
  18 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthSleepingWristTemperatureUploadData extends AppleHealthUploadSample {
  5 + const HealthSleepingWristTemperatureUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.sleepingWristTemperature);
  9 +
  10 + factory HealthSleepingWristTemperatureUploadData.fromJson(
  11 + Map<String, dynamic> json,
  12 + ) {
  13 + return HealthSleepingWristTemperatureUploadData(
  14 + time: healthUploadTimeFromJson(json['time']),
  15 + value: healthUploadDoubleFromJson(json['value']),
  16 + );
  17 + }
  18 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthStandUploadData extends AppleHealthUploadSample {
  5 + const HealthStandUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.stand);
  9 +
  10 + factory HealthStandUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthStandUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthStepCountUploadData extends AppleHealthUploadSample {
  5 + const HealthStepCountUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.steps);
  9 +
  10 + factory HealthStepCountUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthStepCountUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
  1 +import 'apple_health_upload_sample.dart';
  2 +import 'health_upload_data_type.dart';
  3 +
  4 +class HealthWalkingHeartRateUploadData extends AppleHealthUploadSample {
  5 + const HealthWalkingHeartRateUploadData({
  6 + required super.time,
  7 + required super.value,
  8 + }) : super(dataType: HealthUploadDataType.walkingHeartRate);
  9 +
  10 + factory HealthWalkingHeartRateUploadData.fromJson(Map<String, dynamic> json) {
  11 + return HealthWalkingHeartRateUploadData(
  12 + time: healthUploadTimeFromJson(json['time']),
  13 + value: healthUploadDoubleFromJson(json['value']),
  14 + );
  15 + }
  16 +}
1 import 'package:get/get.dart'; 1 import 'package:get/get.dart';
2 2
  3 +import '../apple_health_upload/apple_health_upload_api.dart';
  4 +import '../apple_health_upload/apple_health_upload_tool.dart';
3 import '../../core/config/app_environment_config.dart'; 5 import '../../core/config/app_environment_config.dart';
4 import '../../core/error/app_error_handler.dart'; 6 import '../../core/error/app_error_handler.dart';
5 import '../../core/network/api/config_api.dart'; 7 import '../../core/network/api/config_api.dart';
@@ -71,6 +73,11 @@ void registerUserSessionDeps() { @@ -71,6 +73,11 @@ void registerUserSessionDeps() {
71 /// Health data API and upload pipeline (lazy, recreated after dispose). 73 /// Health data API and upload pipeline (lazy, recreated after dispose).
72 void registerHealthDeps(DioClient dioClient) { 74 void registerHealthDeps(DioClient dioClient) {
73 Get.lazyPut(() => HealthApi(dioClient), fenix: true); 75 Get.lazyPut(() => HealthApi(dioClient), fenix: true);
  76 + Get.lazyPut(() => AppleHealthUploadApi(dioClient), fenix: true);
  77 + Get.lazyPut(
  78 + () => AppleHealthUploadTool(Get.find<AppleHealthUploadApi>()),
  79 + fenix: true,
  80 + );
74 Get.lazyPut( 81 Get.lazyPut(
75 () => HealthKitUploadService(Get.find<HealthApi>()), 82 () => HealthKitUploadService(Get.find<HealthApi>()),
76 fenix: true, 83 fenix: true,
  1 +import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
  2 +import 'package:get/get.dart';
  3 +
  4 +import '../controllers/apple_health_upload_test_controller.dart';
  5 +
  6 +class AppleHealthUploadTestBinding extends Bindings {
  7 + @override
  8 + void dependencies() {
  9 + Get.lazyPut<AppleHealthUploadTestController>(
  10 + () => AppleHealthUploadTestController(
  11 + Get.find<AppleHealthUploadTool>(),
  12 + ),
  13 + );
  14 + }
  15 +}
  1 +import 'dart:convert';
  2 +
  3 +import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_tool.dart';
  4 +import 'package:doublefeel_flutter/app/apple_health_upload/health_kit_upload_mapper.dart';
  5 +import 'package:doublefeel_flutter/app/apple_health_upload/models/apple_health_upload_sample.dart';
  6 +import 'package:doublefeel_flutter/app/apple_health_upload/models/upload_activity_target.dart';
  7 +import 'package:doublefeel_flutter/app/apple_health_upload/models/upload_sleep.dart';
  8 +import 'package:doublefeel_flutter/core/util/app_toast.dart';
  9 +import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
  10 +import 'package:get/get.dart';
  11 +
  12 +class AppleHealthUploadTestController extends GetxController {
  13 + AppleHealthUploadTestController(this._uploadTool);
  14 +
  15 + final AppleHealthUploadTool _uploadTool;
  16 + final HealthKitHostApi _hostApi = HealthKitHostApi();
  17 +
  18 + final logText = ''.obs;
  19 + final permissionTitle = '检测 AppleHealth 权限'.obs;
  20 + final isSyncing = false.obs;
  21 + final isUploading = false.obs;
  22 +
  23 + final List<AppleHealthUploadSample> _commonData = [];
  24 + final List<HealthSleepUploadData> _sleepData = [];
  25 + HealthActivityTargetUploadData? _activityTarget;
  26 +
  27 + Future<void> checkPermission() async {
  28 + _appendLog('开始检测 AppleHealth 权限');
  29 + try {
  30 + final hasPermission = await _hostApi.checkHealthAppAuthorization();
  31 + if (hasPermission) {
  32 + permissionTitle.value = '权限:已授权';
  33 + _appendLog('AppleHealth 权限已授权');
  34 + return;
  35 + }
  36 +
  37 + final granted = await _hostApi.requestHealthClientAuthorization();
  38 + permissionTitle.value = granted ? '权限:已授权' : '权限:未授权';
  39 + _appendLog('AppleHealth 授权请求结果:$granted');
  40 + } catch (error, stackTrace) {
  41 + permissionTitle.value = '权限:检测失败';
  42 + _appendLog('权限检测失败:$error');
  43 + _appendLog(stackTrace.toString());
  44 + }
  45 + }
  46 +
  47 + Future<void> syncHealthData() async {
  48 + if (isSyncing.value) return;
  49 + isSyncing.value = true;
  50 + _commonData.clear();
  51 + _sleepData.clear();
  52 + _activityTarget = null;
  53 + _appendLog('开始同步 HealthKitHostApi 数据');
  54 +
  55 + final endTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
  56 + final startTime = DateTime.now()
  57 + .subtract(const Duration(days: 7))
  58 + .millisecondsSinceEpoch ~/
  59 + 1000;
  60 + _appendLog('同步范围:$startTime -> $endTime');
  61 +
  62 + try {
  63 + await _fetchCommon(
  64 + label: 'hrv',
  65 + fetch: () => _hostApi.fetchHrvData(startTime, endTime),
  66 + );
  67 + await _fetchCommon(
  68 + label: 'heartRate',
  69 + fetch: () => _hostApi.fetchHeartRateData(startTime, endTime),
  70 + );
  71 + await _fetchCommon(
  72 + label: 'walkingHeartRate',
  73 + fetch: () => _hostApi.fetchWalkingHeartRateData(startTime, endTime),
  74 + );
  75 + await _fetchCommon(
  76 + label: 'restingHeartRate',
  77 + fetch: () => _hostApi.fetchRestingHeartRateData(startTime, endTime),
  78 + );
  79 + await _fetchCommon(
  80 + label: 'sleepingHeartRate',
  81 + fetch: () => _hostApi.fetchSleepingHeartRateData(startTime, endTime),
  82 + );
  83 + await _fetchCommon(
  84 + label: 'oxygenSaturation',
  85 + fetch: () => _hostApi.fetchOxygenSaturationData(startTime, endTime),
  86 + );
  87 + await _fetchCommon(
  88 + label: 'activeEnergy',
  89 + fetch: () => _hostApi.fetchActiveEnergyData(startTime, endTime),
  90 + );
  91 + await _fetchCommon(
  92 + label: 'exercise',
  93 + fetch: () => _hostApi.fetchExerciseData(startTime, endTime),
  94 + );
  95 + await _fetchCommon(
  96 + label: 'stand',
  97 + fetch: () => _hostApi.fetchStandData(startTime, endTime),
  98 + );
  99 + await _fetchCommon(
  100 + label: 'stepCount',
  101 + fetch: () => _hostApi.fetchStepCountData(startTime, endTime),
  102 + );
  103 + await _fetchSleep(startTime, endTime);
  104 + await _fetchCommon(
  105 + label: 'sleepingWristTemperature',
  106 + fetch: () =>
  107 + _hostApi.fetchSleepingWristTemperatureData(startTime, endTime),
  108 + );
  109 + await _fetchCommon(
  110 + label: 'respiratoryRate',
  111 + fetch: () => _hostApi.fetchRespiratoryRateData(startTime, endTime),
  112 + );
  113 + await _fetchCommon(
  114 + label: 'irregularHeartRhythm',
  115 + fetch: () => _hostApi.fetchIrregularHeartRhythmData(startTime, endTime),
  116 + );
  117 + await _fetchActivityTarget(startTime, endTime);
  118 +
  119 + _appendLog(
  120 + '同步完成:common=${_commonData.length}, sleep=${_sleepData.length}, '
  121 + 'activityTarget=${_activityTarget == null ? 0 : 1}',
  122 + );
  123 + } catch (error, stackTrace) {
  124 + _appendLog('同步失败:$error');
  125 + _appendLog(stackTrace.toString());
  126 + } finally {
  127 + isSyncing.value = false;
  128 + }
  129 + }
  130 +
  131 + Future<void> uploadHealthData() async {
  132 + if (isUploading.value) return;
  133 + if (_commonData.isEmpty && _sleepData.isEmpty && _activityTarget == null) {
  134 + _appendLog('没有缓存数据,先执行一次同步');
  135 + await syncHealthData();
  136 + }
  137 +
  138 + isUploading.value = true;
  139 + _appendLog('开始上传到服务器');
  140 + try {
  141 + final result = await _uploadTool.upload(
  142 + commonData: _commonData,
  143 + sleepData: _sleepData,
  144 + activityTarget: _activityTarget,
  145 + );
  146 + _appendLog(
  147 + '上传结果:common=${result.commonUploadSuccess}, '
  148 + 'sleep=${result.sleepUploadSuccess}, '
  149 + 'activityTarget=${result.activityTargetUploadSuccess}, '
  150 + 'success=${result.isSuccess}',
  151 + );
  152 + AppToast.show(result.isSuccess ? '上传完成' : '上传失败');
  153 + } catch (error, stackTrace) {
  154 + _appendLog('上传异常:$error');
  155 + _appendLog(stackTrace.toString());
  156 + AppToast.show('上传异常');
  157 + } finally {
  158 + isUploading.value = false;
  159 + }
  160 + }
  161 +
  162 + Future<void> _fetchCommon({
  163 + required String label,
  164 + required Future<List<HealthUploadDataPoint>> Function() fetch,
  165 + }) async {
  166 + final points = await fetch();
  167 + final models = points
  168 + .map((point) => point.toAppleHealthUploadSample())
  169 + .whereType<AppleHealthUploadSample>()
  170 + .toList();
  171 + _commonData.addAll(models);
  172 + _appendDataLog(label, models.map((item) => item.toJson()).toList());
  173 + }
  174 +
  175 + Future<void> _fetchSleep(int startTime, int endTime) async {
  176 + final points = await _hostApi.fetchSleepData(startTime, endTime);
  177 + final models =
  178 + points.map((item) => item.toHealthSleepUploadData()).toList();
  179 + _sleepData.addAll(models);
  180 + _appendDataLog('sleep', models.map((item) => item.toJson()).toList());
  181 + }
  182 +
  183 + Future<void> _fetchActivityTarget(int startTime, int endTime) async {
  184 + final target = await _hostApi.fetchActivityTargetData(startTime, endTime);
  185 + if (target != null) {
  186 + _activityTarget = HealthActivityTargetUploadData(
  187 + move: target.move,
  188 + stand: target.stand,
  189 + );
  190 + }
  191 + _appendDataLog(
  192 + 'activityTarget',
  193 + _activityTarget == null ? [] : [_activityTarget!.toJson()],
  194 + );
  195 + }
  196 +
  197 + void _appendDataLog(String label, List<Map<String, dynamic>> data) {
  198 + _appendLog('[$label] count=${data.length}');
  199 + if (data.isNotEmpty) {
  200 + _appendLog('[$label] first=${jsonEncode(data.first)}');
  201 + _appendLog('[$label] last=${jsonEncode(data.last)}');
  202 + }
  203 + }
  204 +
  205 + void _appendLog(String message) {
  206 + final time = DateTime.now().toIso8601String();
  207 + logText.value = '${logText.value}[$time] $message\n';
  208 + }
  209 +}
  1 +import 'package:flutter/material.dart';
  2 +import 'package:get/get.dart';
  3 +
  4 +import '../controllers/apple_health_upload_test_controller.dart';
  5 +
  6 +class AppleHealthUploadTestView
  7 + extends GetView<AppleHealthUploadTestController> {
  8 + const AppleHealthUploadTestView({super.key});
  9 +
  10 + @override
  11 + Widget build(BuildContext context) {
  12 + return Scaffold(
  13 + appBar: AppBar(
  14 + title: const Text('AppleHealth 上传测试'),
  15 + ),
  16 + body: SafeArea(
  17 + child: Column(
  18 + children: [
  19 + Expanded(
  20 + child: Container(
  21 + width: double.infinity,
  22 + margin: const EdgeInsets.all(12),
  23 + padding: const EdgeInsets.all(12),
  24 + decoration: BoxDecoration(
  25 + color: const Color(0xFF101418),
  26 + borderRadius: BorderRadius.circular(8),
  27 + ),
  28 + child: Obx(
  29 + () => SingleChildScrollView(
  30 + reverse: true,
  31 + child: SelectableText(
  32 + controller.logText.value.isEmpty
  33 + ? '点击底部按钮开始测试 HealthKitHostApiImpl 数据。'
  34 + : controller.logText.value,
  35 + style: const TextStyle(
  36 + color: Color(0xFFE6EDF3),
  37 + fontSize: 12,
  38 + height: 1.45,
  39 + fontFamily: 'Menlo',
  40 + ),
  41 + ),
  42 + ),
  43 + ),
  44 + ),
  45 + ),
  46 + Padding(
  47 + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
  48 + child: Obx(
  49 + () => Row(
  50 + children: [
  51 + Expanded(
  52 + child: _BottomButton(
  53 + label: controller.permissionTitle.value,
  54 + onPressed: controller.checkPermission,
  55 + ),
  56 + ),
  57 + const SizedBox(width: 8),
  58 + Expanded(
  59 + child: _BottomButton(
  60 + label: controller.isSyncing.value ? '同步中' : '同步',
  61 + onPressed: controller.isSyncing.value
  62 + ? null
  63 + : controller.syncHealthData,
  64 + ),
  65 + ),
  66 + const SizedBox(width: 8),
  67 + Expanded(
  68 + child: _BottomButton(
  69 + label: controller.isUploading.value ? '上传中' : '上传',
  70 + onPressed: controller.isUploading.value
  71 + ? null
  72 + : controller.uploadHealthData,
  73 + ),
  74 + ),
  75 + ],
  76 + ),
  77 + ),
  78 + ),
  79 + ],
  80 + ),
  81 + ),
  82 + );
  83 + }
  84 +}
  85 +
  86 +class _BottomButton extends StatelessWidget {
  87 + const _BottomButton({
  88 + required this.label,
  89 + required this.onPressed,
  90 + });
  91 +
  92 + final String label;
  93 + final VoidCallback? onPressed;
  94 +
  95 + @override
  96 + Widget build(BuildContext context) {
  97 + return SizedBox(
  98 + height: 44,
  99 + child: FilledButton(
  100 + onPressed: onPressed,
  101 + style: FilledButton.styleFrom(
  102 + padding: const EdgeInsets.symmetric(horizontal: 8),
  103 + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
  104 + ),
  105 + child: Text(
  106 + label,
  107 + maxLines: 1,
  108 + overflow: TextOverflow.ellipsis,
  109 + textAlign: TextAlign.center,
  110 + style: const TextStyle(fontSize: 13),
  111 + ),
  112 + ),
  113 + );
  114 + }
  115 +}
  1 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
1 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 2 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
2 import 'package:doublefeel_flutter/core/result/app_result.dart'; 3 import 'package:doublefeel_flutter/core/result/app_result.dart';
3 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 4 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
@@ -29,4 +30,15 @@ class MyController extends GetxController { @@ -29,4 +30,15 @@ class MyController extends GetxController {
29 } 30 }
30 } 31 }
31 } 32 }
  33 +
  34 + void goWatchThemePage() {
  35 + Get.toNamed(
  36 + Routes.WATCH_THEME,
  37 + arguments: {'isPremium': false, 'hasCustomThemes': false},
  38 + );
  39 + }
  40 +
  41 + void testAppleHealthUpload() {
  42 + Get.toNamed(Routes.APPLE_HEALTH_UPLOAD_TEST);
  43 + }
32 } 44 }
@@ -12,7 +12,6 @@ import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; @@ -12,7 +12,6 @@ import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
12 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 12 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
13 import 'package:doublefeel_flutter/data/models/user/user_models.dart'; 13 import 'package:doublefeel_flutter/data/models/user/user_models.dart';
14 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 14 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
15 -import 'package:flutter/cupertino.dart';  
16 import 'package:flutter/foundation.dart'; 15 import 'package:flutter/foundation.dart';
17 import 'package:flutter/material.dart'; 16 import 'package:flutter/material.dart';
18 import 'package:get/get.dart'; 17 import 'package:get/get.dart';
@@ -56,10 +55,7 @@ class MyTab extends GetView<MyController> { @@ -56,10 +55,7 @@ class MyTab extends GetView<MyController> {
56 : const _UnlockPremiumCard(), 55 : const _UnlockPremiumCard(),
57 const SizedBox(height: 12), 56 const SizedBox(height: 12),
58 _WatchThemeCard( 57 _WatchThemeCard(
59 - onTap: () => Get.toNamed(  
60 - Routes.WATCH_THEME,  
61 - arguments: {'isPremium': false, 'hasCustomThemes': false},  
62 - ), 58 + onTap: () => controller.goWatchThemePage(),
63 ), 59 ),
64 const SizedBox(height: 12), 60 const SizedBox(height: 12),
65 _SettingsRow( 61 _SettingsRow(
@@ -79,6 +75,13 @@ class MyTab extends GetView<MyController> { @@ -79,6 +75,13 @@ class MyTab extends GetView<MyController> {
79 Get.toNamed(Routes.ROUTE_LIST); 75 Get.toNamed(Routes.ROUTE_LIST);
80 }, 76 },
81 ), 77 ),
  78 + const SizedBox(height: 12),
  79 + _SettingsRow(
  80 + title: 'Apple Health Upload 测试',
  81 + onTap: () {
  82 + controller.testAppleHealthUpload();
  83 + },
  84 + ),
82 ], 85 ],
83 ], 86 ],
84 ); 87 );
@@ -38,6 +38,8 @@ import '../modules/watch_theme/views/watch_theme_preview_view.dart'; @@ -38,6 +38,8 @@ import '../modules/watch_theme/views/watch_theme_preview_view.dart';
38 import '../modules/watch_theme/views/watch_theme_view.dart'; 38 import '../modules/watch_theme/views/watch_theme_view.dart';
39 import '../modules/account_settings/bindings/account_settings_binding.dart'; 39 import '../modules/account_settings/bindings/account_settings_binding.dart';
40 import '../modules/account_settings/views/account_settings_view.dart'; 40 import '../modules/account_settings/views/account_settings_view.dart';
  41 +import '../modules/apple_health_upload_test/bindings/apple_health_upload_test_binding.dart';
  42 +import '../modules/apple_health_upload_test/views/apple_health_upload_test_view.dart';
41 import '../modules/webview/bindings/webview_binding.dart'; 43 import '../modules/webview/bindings/webview_binding.dart';
42 import '../modules/webview/views/webview_page.dart'; 44 import '../modules/webview/views/webview_page.dart';
43 45
@@ -170,5 +172,10 @@ abstract final class AppPages { @@ -170,5 +172,10 @@ abstract final class AppPages {
170 page: () => const AccountSettingsView(), 172 page: () => const AccountSettingsView(),
171 binding: AccountSettingsBinding(), 173 binding: AccountSettingsBinding(),
172 ), 174 ),
  175 + GetPage(
  176 + name: Routes.APPLE_HEALTH_UPLOAD_TEST,
  177 + page: () => const AppleHealthUploadTestView(),
  178 + binding: AppleHealthUploadTestBinding(),
  179 + ),
173 ]; 180 ];
174 } 181 }
@@ -19,6 +19,7 @@ abstract class Routes { @@ -19,6 +19,7 @@ abstract class Routes {
19 static const WATCH_THEME_CUSTOM_PREVIEW = _Paths.WATCH_THEME_CUSTOM_PREVIEW; 19 static const WATCH_THEME_CUSTOM_PREVIEW = _Paths.WATCH_THEME_CUSTOM_PREVIEW;
20 static const ACCOUNT_SETTINGS = _Paths.ACCOUNT_SETTINGS; 20 static const ACCOUNT_SETTINGS = _Paths.ACCOUNT_SETTINGS;
21 static const FRIEND_TREND = _Paths.FRIEND_TREND; 21 static const FRIEND_TREND = _Paths.FRIEND_TREND;
  22 + static const APPLE_HEALTH_UPLOAD_TEST = _Paths.APPLE_HEALTH_UPLOAD_TEST;
22 } 23 }
23 24
24 abstract class _Paths { 25 abstract class _Paths {
@@ -39,4 +40,5 @@ abstract class _Paths { @@ -39,4 +40,5 @@ abstract class _Paths {
39 static const WATCH_THEME_CUSTOM_PREVIEW = '/watch-theme/custom-preview'; 40 static const WATCH_THEME_CUSTOM_PREVIEW = '/watch-theme/custom-preview';
40 static const ACCOUNT_SETTINGS = '/account-settings'; 41 static const ACCOUNT_SETTINGS = '/account-settings';
41 static const FRIEND_TREND = '/friend-trend'; 42 static const FRIEND_TREND = '/friend-trend';
  43 + static const APPLE_HEALTH_UPLOAD_TEST = '/apple-health-upload-test';
42 } 44 }
@@ -21,6 +21,8 @@ abstract final class ApiPaths { @@ -21,6 +21,8 @@ abstract final class ApiPaths {
21 '/client/doublefeel/health/data_upload/common/'; 21 '/client/doublefeel/health/data_upload/common/';
22 static const healthUploadSleep = 22 static const healthUploadSleep =
23 '/client/doublefeel/health/data_upload/sleep/'; 23 '/client/doublefeel/health/data_upload/sleep/';
  24 + static const healthActivityTarget =
  25 + '/client/doublefeel/health/activity_target/';
24 static const healthStatsSleep = '/client/doublefeel/health/statistics/sleep/'; 26 static const healthStatsSleep = '/client/doublefeel/health/statistics/sleep/';
25 static const healthStatsActivity = 27 static const healthStatsActivity =
26 '/client/doublefeel/health/statistics/activity/'; 28 '/client/doublefeel/health/statistics/activity/';
@@ -15,21 +15,22 @@ PlatformException _createConnectionError(String channelName) { @@ -15,21 +15,22 @@ PlatformException _createConnectionError(String channelName) {
15 message: 'Unable to establish connection on channel: "$channelName".', 15 message: 'Unable to establish connection on channel: "$channelName".',
16 ); 16 );
17 } 17 }
  18 +
18 bool _deepEquals(Object? a, Object? b) { 19 bool _deepEquals(Object? a, Object? b) {
19 if (a is List && b is List) { 20 if (a is List && b is List) {
20 return a.length == b.length && 21 return a.length == b.length &&
21 a.indexed 22 a.indexed
22 - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); 23 + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
23 } 24 }
24 if (a is Map && b is Map) { 25 if (a is Map && b is Map) {
25 - return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>  
26 - (b as Map<Object?, Object?>).containsKey(entry.key) &&  
27 - _deepEquals(entry.value, b[entry.key])); 26 + return a.length == b.length &&
  27 + a.entries.every((MapEntry<Object?, Object?> entry) =>
  28 + (b as Map<Object?, Object?>).containsKey(entry.key) &&
  29 + _deepEquals(entry.value, b[entry.key]));
28 } 30 }
29 return a == b; 31 return a == b;
30 } 32 }
31 33
32 -  
33 class HealthUploadResult { 34 class HealthUploadResult {
34 HealthUploadResult({ 35 HealthUploadResult({
35 required this.commonUploadSuccess, 36 required this.commonUploadSuccess,
@@ -52,7 +53,8 @@ class HealthUploadResult { @@ -52,7 +53,8 @@ class HealthUploadResult {
52 } 53 }
53 54
54 Object encode() { 55 Object encode() {
55 - return _toList(); } 56 + return _toList();
  57 + }
56 58
57 static HealthUploadResult decode(Object result) { 59 static HealthUploadResult decode(Object result) {
58 result as List<Object?>; 60 result as List<Object?>;
@@ -77,10 +79,158 @@ class HealthUploadResult { @@ -77,10 +79,158 @@ class HealthUploadResult {
77 79
78 @override 80 @override
79 // ignore: avoid_equals_and_hash_code_on_mutable_classes 81 // ignore: avoid_equals_and_hash_code_on_mutable_classes
80 - int get hashCode => Object.hashAll(_toList())  
81 -; 82 + int get hashCode => Object.hashAll(_toList());
  83 +}
  84 +
  85 +class HealthUploadDataPoint {
  86 + HealthUploadDataPoint({
  87 + required this.dataType,
  88 + required this.time,
  89 + required this.value,
  90 + });
  91 +
  92 + int dataType;
  93 +
  94 + int time;
  95 +
  96 + double value;
  97 +
  98 + List<Object?> _toList() {
  99 + return <Object?>[
  100 + dataType,
  101 + time,
  102 + value,
  103 + ];
  104 + }
  105 +
  106 + Object encode() {
  107 + return _toList();
  108 + }
  109 +
  110 + static HealthUploadDataPoint decode(Object result) {
  111 + result as List<Object?>;
  112 + return HealthUploadDataPoint(
  113 + dataType: result[0]! as int,
  114 + time: result[1]! as int,
  115 + value: result[2]! as double,
  116 + );
  117 + }
  118 +
  119 + @override
  120 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  121 + bool operator ==(Object other) {
  122 + if (other is! HealthUploadDataPoint || other.runtimeType != runtimeType) {
  123 + return false;
  124 + }
  125 + if (identical(this, other)) {
  126 + return true;
  127 + }
  128 + return _deepEquals(encode(), other.encode());
  129 + }
  130 +
  131 + @override
  132 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  133 + int get hashCode => Object.hashAll(_toList());
82 } 134 }
83 135
  136 +class HealthSleepUploadDataPoint {
  137 + HealthSleepUploadDataPoint({
  138 + required this.dataType,
  139 + required this.fromTime,
  140 + required this.toTime,
  141 + });
  142 +
  143 + int dataType;
  144 +
  145 + int fromTime;
  146 +
  147 + int toTime;
  148 +
  149 + List<Object?> _toList() {
  150 + return <Object?>[
  151 + dataType,
  152 + fromTime,
  153 + toTime,
  154 + ];
  155 + }
  156 +
  157 + Object encode() {
  158 + return _toList();
  159 + }
  160 +
  161 + static HealthSleepUploadDataPoint decode(Object result) {
  162 + result as List<Object?>;
  163 + return HealthSleepUploadDataPoint(
  164 + dataType: result[0]! as int,
  165 + fromTime: result[1]! as int,
  166 + toTime: result[2]! as int,
  167 + );
  168 + }
  169 +
  170 + @override
  171 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  172 + bool operator ==(Object other) {
  173 + if (other is! HealthSleepUploadDataPoint ||
  174 + other.runtimeType != runtimeType) {
  175 + return false;
  176 + }
  177 + if (identical(this, other)) {
  178 + return true;
  179 + }
  180 + return _deepEquals(encode(), other.encode());
  181 + }
  182 +
  183 + @override
  184 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  185 + int get hashCode => Object.hashAll(_toList());
  186 +}
  187 +
  188 +class HealthActivityTargetData {
  189 + HealthActivityTargetData({
  190 + this.move,
  191 + this.stand,
  192 + });
  193 +
  194 + int? move;
  195 +
  196 + int? stand;
  197 +
  198 + List<Object?> _toList() {
  199 + return <Object?>[
  200 + move,
  201 + stand,
  202 + ];
  203 + }
  204 +
  205 + Object encode() {
  206 + return _toList();
  207 + }
  208 +
  209 + static HealthActivityTargetData decode(Object result) {
  210 + result as List<Object?>;
  211 + return HealthActivityTargetData(
  212 + move: result[0] as int?,
  213 + stand: result[1] as int?,
  214 + );
  215 + }
  216 +
  217 + @override
  218 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  219 + bool operator ==(Object other) {
  220 + if (other is! HealthActivityTargetData ||
  221 + other.runtimeType != runtimeType) {
  222 + return false;
  223 + }
  224 + if (identical(this, other)) {
  225 + return true;
  226 + }
  227 + return _deepEquals(encode(), other.encode());
  228 + }
  229 +
  230 + @override
  231 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  232 + int get hashCode => Object.hashAll(_toList());
  233 +}
84 234
85 class _PigeonCodec extends StandardMessageCodec { 235 class _PigeonCodec extends StandardMessageCodec {
86 const _PigeonCodec(); 236 const _PigeonCodec();
@@ -89,9 +239,18 @@ class _PigeonCodec extends StandardMessageCodec { @@ -89,9 +239,18 @@ class _PigeonCodec extends StandardMessageCodec {
89 if (value is int) { 239 if (value is int) {
90 buffer.putUint8(4); 240 buffer.putUint8(4);
91 buffer.putInt64(value); 241 buffer.putInt64(value);
92 - } else if (value is HealthUploadResult) { 242 + } else if (value is HealthUploadResult) {
93 buffer.putUint8(129); 243 buffer.putUint8(129);
94 writeValue(buffer, value.encode()); 244 writeValue(buffer, value.encode());
  245 + } else if (value is HealthUploadDataPoint) {
  246 + buffer.putUint8(130);
  247 + writeValue(buffer, value.encode());
  248 + } else if (value is HealthSleepUploadDataPoint) {
  249 + buffer.putUint8(131);
  250 + writeValue(buffer, value.encode());
  251 + } else if (value is HealthActivityTargetData) {
  252 + buffer.putUint8(132);
  253 + writeValue(buffer, value.encode());
95 } else { 254 } else {
96 super.writeValue(buffer, value); 255 super.writeValue(buffer, value);
97 } 256 }
@@ -100,8 +259,14 @@ class _PigeonCodec extends StandardMessageCodec { @@ -100,8 +259,14 @@ class _PigeonCodec extends StandardMessageCodec {
100 @override 259 @override
101 Object? readValueOfType(int type, ReadBuffer buffer) { 260 Object? readValueOfType(int type, ReadBuffer buffer) {
102 switch (type) { 261 switch (type) {
103 - case 129: 262 + case 129:
104 return HealthUploadResult.decode(readValue(buffer)!); 263 return HealthUploadResult.decode(readValue(buffer)!);
  264 + case 130:
  265 + return HealthUploadDataPoint.decode(readValue(buffer)!);
  266 + case 131:
  267 + return HealthSleepUploadDataPoint.decode(readValue(buffer)!);
  268 + case 132:
  269 + return HealthActivityTargetData.decode(readValue(buffer)!);
105 default: 270 default:
106 return super.readValueOfType(type, buffer); 271 return super.readValueOfType(type, buffer);
107 } 272 }
@@ -112,9 +277,11 @@ class HealthKitHostApi { @@ -112,9 +277,11 @@ class HealthKitHostApi {
112 /// Constructor for [HealthKitHostApi]. The [binaryMessenger] named argument is 277 /// Constructor for [HealthKitHostApi]. The [binaryMessenger] named argument is
113 /// available for dependency injection. If it is left null, the default 278 /// available for dependency injection. If it is left null, the default
114 /// BinaryMessenger will be used which routes to the host platform. 279 /// BinaryMessenger will be used which routes to the host platform.
115 - HealthKitHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) 280 + HealthKitHostApi(
  281 + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
116 : pigeonVar_binaryMessenger = binaryMessenger, 282 : pigeonVar_binaryMessenger = binaryMessenger,
117 - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; 283 + pigeonVar_messageChannelSuffix =
  284 + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
118 final BinaryMessenger? pigeonVar_binaryMessenger; 285 final BinaryMessenger? pigeonVar_binaryMessenger;
119 286
120 static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); 287 static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -122,8 +289,10 @@ class HealthKitHostApi { @@ -122,8 +289,10 @@ class HealthKitHostApi {
122 final String pigeonVar_messageChannelSuffix; 289 final String pigeonVar_messageChannelSuffix;
123 290
124 Future<bool> checkHealthAppAuthorization() async { 291 Future<bool> checkHealthAppAuthorization() async {
125 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$pigeonVar_messageChannelSuffix';  
126 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 292 + final String pigeonVar_channelName =
  293 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.checkHealthAppAuthorization$pigeonVar_messageChannelSuffix';
  294 + final BasicMessageChannel<Object?> pigeonVar_channel =
  295 + BasicMessageChannel<Object?>(
127 pigeonVar_channelName, 296 pigeonVar_channelName,
128 pigeonChannelCodec, 297 pigeonChannelCodec,
129 binaryMessenger: pigeonVar_binaryMessenger, 298 binaryMessenger: pigeonVar_binaryMessenger,
@@ -150,8 +319,10 @@ class HealthKitHostApi { @@ -150,8 +319,10 @@ class HealthKitHostApi {
150 } 319 }
151 320
152 Future<String> getHealthServerAuthUrl() async { 321 Future<String> getHealthServerAuthUrl() async {
153 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl$pigeonVar_messageChannelSuffix';  
154 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 322 + final String pigeonVar_channelName =
  323 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.getHealthServerAuthUrl$pigeonVar_messageChannelSuffix';
  324 + final BasicMessageChannel<Object?> pigeonVar_channel =
  325 + BasicMessageChannel<Object?>(
155 pigeonVar_channelName, 326 pigeonVar_channelName,
156 pigeonChannelCodec, 327 pigeonChannelCodec,
157 binaryMessenger: pigeonVar_binaryMessenger, 328 binaryMessenger: pigeonVar_binaryMessenger,
@@ -179,8 +350,10 @@ class HealthKitHostApi { @@ -179,8 +350,10 @@ class HealthKitHostApi {
179 350
180 /// Opens Huawei Health client authorization UI. Returns whether user granted. 351 /// Opens Huawei Health client authorization UI. Returns whether user granted.
181 Future<bool> requestHealthClientAuthorization() async { 352 Future<bool> requestHealthClientAuthorization() async {
182 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization$pigeonVar_messageChannelSuffix';  
183 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 353 + final String pigeonVar_channelName =
  354 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.requestHealthClientAuthorization$pigeonVar_messageChannelSuffix';
  355 + final BasicMessageChannel<Object?> pigeonVar_channel =
  356 + BasicMessageChannel<Object?>(
184 pigeonVar_channelName, 357 pigeonVar_channelName,
185 pigeonChannelCodec, 358 pigeonChannelCodec,
186 binaryMessenger: pigeonVar_binaryMessenger, 359 binaryMessenger: pigeonVar_binaryMessenger,
@@ -207,8 +380,10 @@ class HealthKitHostApi { @@ -207,8 +380,10 @@ class HealthKitHostApi {
207 } 380 }
208 381
209 Future<bool> cancelHealthAppAuthorization() async { 382 Future<bool> cancelHealthAppAuthorization() async {
210 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization$pigeonVar_messageChannelSuffix';  
211 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 383 + final String pigeonVar_channelName =
  384 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelHealthAppAuthorization$pigeonVar_messageChannelSuffix';
  385 + final BasicMessageChannel<Object?> pigeonVar_channel =
  386 + BasicMessageChannel<Object?>(
212 pigeonVar_channelName, 387 pigeonVar_channelName,
213 pigeonChannelCodec, 388 pigeonChannelCodec,
214 binaryMessenger: pigeonVar_binaryMessenger, 389 binaryMessenger: pigeonVar_binaryMessenger,
@@ -236,8 +411,10 @@ class HealthKitHostApi { @@ -236,8 +411,10 @@ class HealthKitHostApi {
236 411
237 /// Runs native health read and server upload pipeline. 412 /// Runs native health read and server upload pipeline.
238 Future<HealthUploadResult> performHealthUpload() async { 413 Future<HealthUploadResult> performHealthUpload() async {
239 - final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.performHealthUpload$pigeonVar_messageChannelSuffix';  
240 - final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( 414 + final String pigeonVar_channelName =
  415 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.performHealthUpload$pigeonVar_messageChannelSuffix';
  416 + final BasicMessageChannel<Object?> pigeonVar_channel =
  417 + BasicMessageChannel<Object?>(
241 pigeonVar_channelName, 418 pigeonVar_channelName,
242 pigeonChannelCodec, 419 pigeonChannelCodec,
243 binaryMessenger: pigeonVar_binaryMessenger, 420 binaryMessenger: pigeonVar_binaryMessenger,
@@ -262,4 +439,493 @@ class HealthKitHostApi { @@ -262,4 +439,493 @@ class HealthKitHostApi {
262 return (pigeonVar_replyList[0] as HealthUploadResult?)!; 439 return (pigeonVar_replyList[0] as HealthUploadResult?)!;
263 } 440 }
264 } 441 }
  442 +
  443 + Future<List<HealthUploadDataPoint>> fetchHrvData(
  444 + int startTime, int endTime) async {
  445 + final String pigeonVar_channelName =
  446 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHrvData$pigeonVar_messageChannelSuffix';
  447 + final BasicMessageChannel<Object?> pigeonVar_channel =
  448 + BasicMessageChannel<Object?>(
  449 + pigeonVar_channelName,
  450 + pigeonChannelCodec,
  451 + binaryMessenger: pigeonVar_binaryMessenger,
  452 + );
  453 + final Future<Object?> pigeonVar_sendFuture =
  454 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  455 + final List<Object?>? pigeonVar_replyList =
  456 + await pigeonVar_sendFuture as List<Object?>?;
  457 + if (pigeonVar_replyList == null) {
  458 + throw _createConnectionError(pigeonVar_channelName);
  459 + } else if (pigeonVar_replyList.length > 1) {
  460 + throw PlatformException(
  461 + code: pigeonVar_replyList[0]! as String,
  462 + message: pigeonVar_replyList[1] as String?,
  463 + details: pigeonVar_replyList[2],
  464 + );
  465 + } else if (pigeonVar_replyList[0] == null) {
  466 + throw PlatformException(
  467 + code: 'null-error',
  468 + message: 'Host platform returned null value for non-null return value.',
  469 + );
  470 + } else {
  471 + return (pigeonVar_replyList[0] as List<Object?>?)!
  472 + .cast<HealthUploadDataPoint>();
  473 + }
  474 + }
  475 +
  476 + Future<List<HealthUploadDataPoint>> fetchHeartRateData(
  477 + int startTime, int endTime) async {
  478 + final String pigeonVar_channelName =
  479 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchHeartRateData$pigeonVar_messageChannelSuffix';
  480 + final BasicMessageChannel<Object?> pigeonVar_channel =
  481 + BasicMessageChannel<Object?>(
  482 + pigeonVar_channelName,
  483 + pigeonChannelCodec,
  484 + binaryMessenger: pigeonVar_binaryMessenger,
  485 + );
  486 + final Future<Object?> pigeonVar_sendFuture =
  487 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  488 + final List<Object?>? pigeonVar_replyList =
  489 + await pigeonVar_sendFuture as List<Object?>?;
  490 + if (pigeonVar_replyList == null) {
  491 + throw _createConnectionError(pigeonVar_channelName);
  492 + } else if (pigeonVar_replyList.length > 1) {
  493 + throw PlatformException(
  494 + code: pigeonVar_replyList[0]! as String,
  495 + message: pigeonVar_replyList[1] as String?,
  496 + details: pigeonVar_replyList[2],
  497 + );
  498 + } else if (pigeonVar_replyList[0] == null) {
  499 + throw PlatformException(
  500 + code: 'null-error',
  501 + message: 'Host platform returned null value for non-null return value.',
  502 + );
  503 + } else {
  504 + return (pigeonVar_replyList[0] as List<Object?>?)!
  505 + .cast<HealthUploadDataPoint>();
  506 + }
  507 + }
  508 +
  509 + Future<List<HealthUploadDataPoint>> fetchWalkingHeartRateData(
  510 + int startTime, int endTime) async {
  511 + final String pigeonVar_channelName =
  512 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchWalkingHeartRateData$pigeonVar_messageChannelSuffix';
  513 + final BasicMessageChannel<Object?> pigeonVar_channel =
  514 + BasicMessageChannel<Object?>(
  515 + pigeonVar_channelName,
  516 + pigeonChannelCodec,
  517 + binaryMessenger: pigeonVar_binaryMessenger,
  518 + );
  519 + final Future<Object?> pigeonVar_sendFuture =
  520 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  521 + final List<Object?>? pigeonVar_replyList =
  522 + await pigeonVar_sendFuture as List<Object?>?;
  523 + if (pigeonVar_replyList == null) {
  524 + throw _createConnectionError(pigeonVar_channelName);
  525 + } else if (pigeonVar_replyList.length > 1) {
  526 + throw PlatformException(
  527 + code: pigeonVar_replyList[0]! as String,
  528 + message: pigeonVar_replyList[1] as String?,
  529 + details: pigeonVar_replyList[2],
  530 + );
  531 + } else if (pigeonVar_replyList[0] == null) {
  532 + throw PlatformException(
  533 + code: 'null-error',
  534 + message: 'Host platform returned null value for non-null return value.',
  535 + );
  536 + } else {
  537 + return (pigeonVar_replyList[0] as List<Object?>?)!
  538 + .cast<HealthUploadDataPoint>();
  539 + }
  540 + }
  541 +
  542 + Future<List<HealthUploadDataPoint>> fetchRestingHeartRateData(
  543 + int startTime, int endTime) async {
  544 + final String pigeonVar_channelName =
  545 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchRestingHeartRateData$pigeonVar_messageChannelSuffix';
  546 + final BasicMessageChannel<Object?> pigeonVar_channel =
  547 + BasicMessageChannel<Object?>(
  548 + pigeonVar_channelName,
  549 + pigeonChannelCodec,
  550 + binaryMessenger: pigeonVar_binaryMessenger,
  551 + );
  552 + final Future<Object?> pigeonVar_sendFuture =
  553 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  554 + final List<Object?>? pigeonVar_replyList =
  555 + await pigeonVar_sendFuture as List<Object?>?;
  556 + if (pigeonVar_replyList == null) {
  557 + throw _createConnectionError(pigeonVar_channelName);
  558 + } else if (pigeonVar_replyList.length > 1) {
  559 + throw PlatformException(
  560 + code: pigeonVar_replyList[0]! as String,
  561 + message: pigeonVar_replyList[1] as String?,
  562 + details: pigeonVar_replyList[2],
  563 + );
  564 + } else if (pigeonVar_replyList[0] == null) {
  565 + throw PlatformException(
  566 + code: 'null-error',
  567 + message: 'Host platform returned null value for non-null return value.',
  568 + );
  569 + } else {
  570 + return (pigeonVar_replyList[0] as List<Object?>?)!
  571 + .cast<HealthUploadDataPoint>();
  572 + }
  573 + }
  574 +
  575 + Future<List<HealthUploadDataPoint>> fetchSleepingHeartRateData(
  576 + int startTime, int endTime) async {
  577 + final String pigeonVar_channelName =
  578 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepingHeartRateData$pigeonVar_messageChannelSuffix';
  579 + final BasicMessageChannel<Object?> pigeonVar_channel =
  580 + BasicMessageChannel<Object?>(
  581 + pigeonVar_channelName,
  582 + pigeonChannelCodec,
  583 + binaryMessenger: pigeonVar_binaryMessenger,
  584 + );
  585 + final Future<Object?> pigeonVar_sendFuture =
  586 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  587 + final List<Object?>? pigeonVar_replyList =
  588 + await pigeonVar_sendFuture as List<Object?>?;
  589 + if (pigeonVar_replyList == null) {
  590 + throw _createConnectionError(pigeonVar_channelName);
  591 + } else if (pigeonVar_replyList.length > 1) {
  592 + throw PlatformException(
  593 + code: pigeonVar_replyList[0]! as String,
  594 + message: pigeonVar_replyList[1] as String?,
  595 + details: pigeonVar_replyList[2],
  596 + );
  597 + } else if (pigeonVar_replyList[0] == null) {
  598 + throw PlatformException(
  599 + code: 'null-error',
  600 + message: 'Host platform returned null value for non-null return value.',
  601 + );
  602 + } else {
  603 + return (pigeonVar_replyList[0] as List<Object?>?)!
  604 + .cast<HealthUploadDataPoint>();
  605 + }
  606 + }
  607 +
  608 + Future<List<HealthUploadDataPoint>> fetchOxygenSaturationData(
  609 + int startTime, int endTime) async {
  610 + final String pigeonVar_channelName =
  611 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchOxygenSaturationData$pigeonVar_messageChannelSuffix';
  612 + final BasicMessageChannel<Object?> pigeonVar_channel =
  613 + BasicMessageChannel<Object?>(
  614 + pigeonVar_channelName,
  615 + pigeonChannelCodec,
  616 + binaryMessenger: pigeonVar_binaryMessenger,
  617 + );
  618 + final Future<Object?> pigeonVar_sendFuture =
  619 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  620 + final List<Object?>? pigeonVar_replyList =
  621 + await pigeonVar_sendFuture as List<Object?>?;
  622 + if (pigeonVar_replyList == null) {
  623 + throw _createConnectionError(pigeonVar_channelName);
  624 + } else if (pigeonVar_replyList.length > 1) {
  625 + throw PlatformException(
  626 + code: pigeonVar_replyList[0]! as String,
  627 + message: pigeonVar_replyList[1] as String?,
  628 + details: pigeonVar_replyList[2],
  629 + );
  630 + } else if (pigeonVar_replyList[0] == null) {
  631 + throw PlatformException(
  632 + code: 'null-error',
  633 + message: 'Host platform returned null value for non-null return value.',
  634 + );
  635 + } else {
  636 + return (pigeonVar_replyList[0] as List<Object?>?)!
  637 + .cast<HealthUploadDataPoint>();
  638 + }
  639 + }
  640 +
  641 + Future<List<HealthUploadDataPoint>> fetchActiveEnergyData(
  642 + int startTime, int endTime) async {
  643 + final String pigeonVar_channelName =
  644 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchActiveEnergyData$pigeonVar_messageChannelSuffix';
  645 + final BasicMessageChannel<Object?> pigeonVar_channel =
  646 + BasicMessageChannel<Object?>(
  647 + pigeonVar_channelName,
  648 + pigeonChannelCodec,
  649 + binaryMessenger: pigeonVar_binaryMessenger,
  650 + );
  651 + final Future<Object?> pigeonVar_sendFuture =
  652 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  653 + final List<Object?>? pigeonVar_replyList =
  654 + await pigeonVar_sendFuture as List<Object?>?;
  655 + if (pigeonVar_replyList == null) {
  656 + throw _createConnectionError(pigeonVar_channelName);
  657 + } else if (pigeonVar_replyList.length > 1) {
  658 + throw PlatformException(
  659 + code: pigeonVar_replyList[0]! as String,
  660 + message: pigeonVar_replyList[1] as String?,
  661 + details: pigeonVar_replyList[2],
  662 + );
  663 + } else if (pigeonVar_replyList[0] == null) {
  664 + throw PlatformException(
  665 + code: 'null-error',
  666 + message: 'Host platform returned null value for non-null return value.',
  667 + );
  668 + } else {
  669 + return (pigeonVar_replyList[0] as List<Object?>?)!
  670 + .cast<HealthUploadDataPoint>();
  671 + }
  672 + }
  673 +
  674 + Future<List<HealthUploadDataPoint>> fetchExerciseData(
  675 + int startTime, int endTime) async {
  676 + final String pigeonVar_channelName =
  677 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchExerciseData$pigeonVar_messageChannelSuffix';
  678 + final BasicMessageChannel<Object?> pigeonVar_channel =
  679 + BasicMessageChannel<Object?>(
  680 + pigeonVar_channelName,
  681 + pigeonChannelCodec,
  682 + binaryMessenger: pigeonVar_binaryMessenger,
  683 + );
  684 + final Future<Object?> pigeonVar_sendFuture =
  685 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  686 + final List<Object?>? pigeonVar_replyList =
  687 + await pigeonVar_sendFuture as List<Object?>?;
  688 + if (pigeonVar_replyList == null) {
  689 + throw _createConnectionError(pigeonVar_channelName);
  690 + } else if (pigeonVar_replyList.length > 1) {
  691 + throw PlatformException(
  692 + code: pigeonVar_replyList[0]! as String,
  693 + message: pigeonVar_replyList[1] as String?,
  694 + details: pigeonVar_replyList[2],
  695 + );
  696 + } else if (pigeonVar_replyList[0] == null) {
  697 + throw PlatformException(
  698 + code: 'null-error',
  699 + message: 'Host platform returned null value for non-null return value.',
  700 + );
  701 + } else {
  702 + return (pigeonVar_replyList[0] as List<Object?>?)!
  703 + .cast<HealthUploadDataPoint>();
  704 + }
  705 + }
  706 +
  707 + Future<List<HealthUploadDataPoint>> fetchStandData(
  708 + int startTime, int endTime) async {
  709 + final String pigeonVar_channelName =
  710 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchStandData$pigeonVar_messageChannelSuffix';
  711 + final BasicMessageChannel<Object?> pigeonVar_channel =
  712 + BasicMessageChannel<Object?>(
  713 + pigeonVar_channelName,
  714 + pigeonChannelCodec,
  715 + binaryMessenger: pigeonVar_binaryMessenger,
  716 + );
  717 + final Future<Object?> pigeonVar_sendFuture =
  718 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  719 + final List<Object?>? pigeonVar_replyList =
  720 + await pigeonVar_sendFuture as List<Object?>?;
  721 + if (pigeonVar_replyList == null) {
  722 + throw _createConnectionError(pigeonVar_channelName);
  723 + } else if (pigeonVar_replyList.length > 1) {
  724 + throw PlatformException(
  725 + code: pigeonVar_replyList[0]! as String,
  726 + message: pigeonVar_replyList[1] as String?,
  727 + details: pigeonVar_replyList[2],
  728 + );
  729 + } else if (pigeonVar_replyList[0] == null) {
  730 + throw PlatformException(
  731 + code: 'null-error',
  732 + message: 'Host platform returned null value for non-null return value.',
  733 + );
  734 + } else {
  735 + return (pigeonVar_replyList[0] as List<Object?>?)!
  736 + .cast<HealthUploadDataPoint>();
  737 + }
  738 + }
  739 +
  740 + Future<List<HealthUploadDataPoint>> fetchStepCountData(
  741 + int startTime, int endTime) async {
  742 + final String pigeonVar_channelName =
  743 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchStepCountData$pigeonVar_messageChannelSuffix';
  744 + final BasicMessageChannel<Object?> pigeonVar_channel =
  745 + BasicMessageChannel<Object?>(
  746 + pigeonVar_channelName,
  747 + pigeonChannelCodec,
  748 + binaryMessenger: pigeonVar_binaryMessenger,
  749 + );
  750 + final Future<Object?> pigeonVar_sendFuture =
  751 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  752 + final List<Object?>? pigeonVar_replyList =
  753 + await pigeonVar_sendFuture as List<Object?>?;
  754 + if (pigeonVar_replyList == null) {
  755 + throw _createConnectionError(pigeonVar_channelName);
  756 + } else if (pigeonVar_replyList.length > 1) {
  757 + throw PlatformException(
  758 + code: pigeonVar_replyList[0]! as String,
  759 + message: pigeonVar_replyList[1] as String?,
  760 + details: pigeonVar_replyList[2],
  761 + );
  762 + } else if (pigeonVar_replyList[0] == null) {
  763 + throw PlatformException(
  764 + code: 'null-error',
  765 + message: 'Host platform returned null value for non-null return value.',
  766 + );
  767 + } else {
  768 + return (pigeonVar_replyList[0] as List<Object?>?)!
  769 + .cast<HealthUploadDataPoint>();
  770 + }
  771 + }
  772 +
  773 + Future<List<HealthSleepUploadDataPoint>> fetchSleepData(
  774 + int startTime, int endTime) async {
  775 + final String pigeonVar_channelName =
  776 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepData$pigeonVar_messageChannelSuffix';
  777 + final BasicMessageChannel<Object?> pigeonVar_channel =
  778 + BasicMessageChannel<Object?>(
  779 + pigeonVar_channelName,
  780 + pigeonChannelCodec,
  781 + binaryMessenger: pigeonVar_binaryMessenger,
  782 + );
  783 + final Future<Object?> pigeonVar_sendFuture =
  784 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  785 + final List<Object?>? pigeonVar_replyList =
  786 + await pigeonVar_sendFuture as List<Object?>?;
  787 + if (pigeonVar_replyList == null) {
  788 + throw _createConnectionError(pigeonVar_channelName);
  789 + } else if (pigeonVar_replyList.length > 1) {
  790 + throw PlatformException(
  791 + code: pigeonVar_replyList[0]! as String,
  792 + message: pigeonVar_replyList[1] as String?,
  793 + details: pigeonVar_replyList[2],
  794 + );
  795 + } else if (pigeonVar_replyList[0] == null) {
  796 + throw PlatformException(
  797 + code: 'null-error',
  798 + message: 'Host platform returned null value for non-null return value.',
  799 + );
  800 + } else {
  801 + return (pigeonVar_replyList[0] as List<Object?>?)!
  802 + .cast<HealthSleepUploadDataPoint>();
  803 + }
  804 + }
  805 +
  806 + Future<List<HealthUploadDataPoint>> fetchSleepingWristTemperatureData(
  807 + int startTime, int endTime) async {
  808 + final String pigeonVar_channelName =
  809 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchSleepingWristTemperatureData$pigeonVar_messageChannelSuffix';
  810 + final BasicMessageChannel<Object?> pigeonVar_channel =
  811 + BasicMessageChannel<Object?>(
  812 + pigeonVar_channelName,
  813 + pigeonChannelCodec,
  814 + binaryMessenger: pigeonVar_binaryMessenger,
  815 + );
  816 + final Future<Object?> pigeonVar_sendFuture =
  817 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  818 + final List<Object?>? pigeonVar_replyList =
  819 + await pigeonVar_sendFuture as List<Object?>?;
  820 + if (pigeonVar_replyList == null) {
  821 + throw _createConnectionError(pigeonVar_channelName);
  822 + } else if (pigeonVar_replyList.length > 1) {
  823 + throw PlatformException(
  824 + code: pigeonVar_replyList[0]! as String,
  825 + message: pigeonVar_replyList[1] as String?,
  826 + details: pigeonVar_replyList[2],
  827 + );
  828 + } else if (pigeonVar_replyList[0] == null) {
  829 + throw PlatformException(
  830 + code: 'null-error',
  831 + message: 'Host platform returned null value for non-null return value.',
  832 + );
  833 + } else {
  834 + return (pigeonVar_replyList[0] as List<Object?>?)!
  835 + .cast<HealthUploadDataPoint>();
  836 + }
  837 + }
  838 +
  839 + Future<List<HealthUploadDataPoint>> fetchRespiratoryRateData(
  840 + int startTime, int endTime) async {
  841 + final String pigeonVar_channelName =
  842 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchRespiratoryRateData$pigeonVar_messageChannelSuffix';
  843 + final BasicMessageChannel<Object?> pigeonVar_channel =
  844 + BasicMessageChannel<Object?>(
  845 + pigeonVar_channelName,
  846 + pigeonChannelCodec,
  847 + binaryMessenger: pigeonVar_binaryMessenger,
  848 + );
  849 + final Future<Object?> pigeonVar_sendFuture =
  850 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  851 + final List<Object?>? pigeonVar_replyList =
  852 + await pigeonVar_sendFuture as List<Object?>?;
  853 + if (pigeonVar_replyList == null) {
  854 + throw _createConnectionError(pigeonVar_channelName);
  855 + } else if (pigeonVar_replyList.length > 1) {
  856 + throw PlatformException(
  857 + code: pigeonVar_replyList[0]! as String,
  858 + message: pigeonVar_replyList[1] as String?,
  859 + details: pigeonVar_replyList[2],
  860 + );
  861 + } else if (pigeonVar_replyList[0] == null) {
  862 + throw PlatformException(
  863 + code: 'null-error',
  864 + message: 'Host platform returned null value for non-null return value.',
  865 + );
  866 + } else {
  867 + return (pigeonVar_replyList[0] as List<Object?>?)!
  868 + .cast<HealthUploadDataPoint>();
  869 + }
  870 + }
  871 +
  872 + Future<List<HealthUploadDataPoint>> fetchIrregularHeartRhythmData(
  873 + int startTime, int endTime) async {
  874 + final String pigeonVar_channelName =
  875 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchIrregularHeartRhythmData$pigeonVar_messageChannelSuffix';
  876 + final BasicMessageChannel<Object?> pigeonVar_channel =
  877 + BasicMessageChannel<Object?>(
  878 + pigeonVar_channelName,
  879 + pigeonChannelCodec,
  880 + binaryMessenger: pigeonVar_binaryMessenger,
  881 + );
  882 + final Future<Object?> pigeonVar_sendFuture =
  883 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  884 + final List<Object?>? pigeonVar_replyList =
  885 + await pigeonVar_sendFuture as List<Object?>?;
  886 + if (pigeonVar_replyList == null) {
  887 + throw _createConnectionError(pigeonVar_channelName);
  888 + } else if (pigeonVar_replyList.length > 1) {
  889 + throw PlatformException(
  890 + code: pigeonVar_replyList[0]! as String,
  891 + message: pigeonVar_replyList[1] as String?,
  892 + details: pigeonVar_replyList[2],
  893 + );
  894 + } else if (pigeonVar_replyList[0] == null) {
  895 + throw PlatformException(
  896 + code: 'null-error',
  897 + message: 'Host platform returned null value for non-null return value.',
  898 + );
  899 + } else {
  900 + return (pigeonVar_replyList[0] as List<Object?>?)!
  901 + .cast<HealthUploadDataPoint>();
  902 + }
  903 + }
  904 +
  905 + Future<HealthActivityTargetData?> fetchActivityTargetData(
  906 + int startTime, int endTime) async {
  907 + final String pigeonVar_channelName =
  908 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.fetchActivityTargetData$pigeonVar_messageChannelSuffix';
  909 + final BasicMessageChannel<Object?> pigeonVar_channel =
  910 + BasicMessageChannel<Object?>(
  911 + pigeonVar_channelName,
  912 + pigeonChannelCodec,
  913 + binaryMessenger: pigeonVar_binaryMessenger,
  914 + );
  915 + final Future<Object?> pigeonVar_sendFuture =
  916 + pigeonVar_channel.send(<Object?>[startTime, endTime]);
  917 + final List<Object?>? pigeonVar_replyList =
  918 + await pigeonVar_sendFuture as List<Object?>?;
  919 + if (pigeonVar_replyList == null) {
  920 + throw _createConnectionError(pigeonVar_channelName);
  921 + } else if (pigeonVar_replyList.length > 1) {
  922 + throw PlatformException(
  923 + code: pigeonVar_replyList[0]! as String,
  924 + message: pigeonVar_replyList[1] as String?,
  925 + details: pigeonVar_replyList[2],
  926 + );
  927 + } else {
  928 + return (pigeonVar_replyList[0] as HealthActivityTargetData?);
  929 + }
  930 + }
265 } 931 }
@@ -15,22 +15,21 @@ PlatformException _createConnectionError(String channelName) { @@ -15,22 +15,21 @@ PlatformException _createConnectionError(String channelName) {
15 message: 'Unable to establish connection on channel: "$channelName".', 15 message: 'Unable to establish connection on channel: "$channelName".',
16 ); 16 );
17 } 17 }
18 -  
19 bool _deepEquals(Object? a, Object? b) { 18 bool _deepEquals(Object? a, Object? b) {
20 if (a is List && b is List) { 19 if (a is List && b is List) {
21 return a.length == b.length && 20 return a.length == b.length &&
22 a.indexed 21 a.indexed
23 - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); 22 + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
24 } 23 }
25 if (a is Map && b is Map) { 24 if (a is Map && b is Map) {
26 - return a.length == b.length &&  
27 - a.entries.every((MapEntry<Object?, Object?> entry) =>  
28 - (b as Map<Object?, Object?>).containsKey(entry.key) &&  
29 - _deepEquals(entry.value, b[entry.key])); 25 + return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
  26 + (b as Map<Object?, Object?>).containsKey(entry.key) &&
  27 + _deepEquals(entry.value, b[entry.key]));
30 } 28 }
31 return a == b; 29 return a == b;
32 } 30 }
33 31
  32 +
34 class WearDeviceInfo { 33 class WearDeviceInfo {
35 WearDeviceInfo({ 34 WearDeviceInfo({
36 this.deviceId, 35 this.deviceId,
@@ -53,8 +52,7 @@ class WearDeviceInfo { @@ -53,8 +52,7 @@ class WearDeviceInfo {
53 } 52 }
54 53
55 Object encode() { 54 Object encode() {
56 - return _toList();  
57 - } 55 + return _toList(); }
58 56
59 static WearDeviceInfo decode(Object result) { 57 static WearDeviceInfo decode(Object result) {
60 result as List<Object?>; 58 result as List<Object?>;
@@ -79,9 +77,11 @@ class WearDeviceInfo { @@ -79,9 +77,11 @@ class WearDeviceInfo {
79 77
80 @override 78 @override
81 // ignore: avoid_equals_and_hash_code_on_mutable_classes 79 // ignore: avoid_equals_and_hash_code_on_mutable_classes
82 - int get hashCode => Object.hashAll(_toList()); 80 + int get hashCode => Object.hashAll(_toList())
  81 +;
83 } 82 }
84 83
  84 +
85 class _PigeonCodec extends StandardMessageCodec { 85 class _PigeonCodec extends StandardMessageCodec {
86 const _PigeonCodec(); 86 const _PigeonCodec();
87 @override 87 @override
@@ -89,7 +89,7 @@ class _PigeonCodec extends StandardMessageCodec { @@ -89,7 +89,7 @@ class _PigeonCodec extends StandardMessageCodec {
89 if (value is int) { 89 if (value is int) {
90 buffer.putUint8(4); 90 buffer.putUint8(4);
91 buffer.putInt64(value); 91 buffer.putInt64(value);
92 - } else if (value is WearDeviceInfo) { 92 + } else if (value is WearDeviceInfo) {
93 buffer.putUint8(129); 93 buffer.putUint8(129);
94 writeValue(buffer, value.encode()); 94 writeValue(buffer, value.encode());
95 } else { 95 } else {
@@ -100,7 +100,7 @@ class _PigeonCodec extends StandardMessageCodec { @@ -100,7 +100,7 @@ class _PigeonCodec extends StandardMessageCodec {
100 @override 100 @override
101 Object? readValueOfType(int type, ReadBuffer buffer) { 101 Object? readValueOfType(int type, ReadBuffer buffer) {
102 switch (type) { 102 switch (type) {
103 - case 129: 103 + case 129:
104 return WearDeviceInfo.decode(readValue(buffer)!); 104 return WearDeviceInfo.decode(readValue(buffer)!);
105 default: 105 default:
106 return super.readValueOfType(type, buffer); 106 return super.readValueOfType(type, buffer);
@@ -112,11 +112,9 @@ class WearEngineHostApi { @@ -112,11 +112,9 @@ class WearEngineHostApi {
112 /// Constructor for [WearEngineHostApi]. The [binaryMessenger] named argument is 112 /// Constructor for [WearEngineHostApi]. The [binaryMessenger] named argument is
113 /// available for dependency injection. If it is left null, the default 113 /// available for dependency injection. If it is left null, the default
114 /// BinaryMessenger will be used which routes to the host platform. 114 /// BinaryMessenger will be used which routes to the host platform.
115 - WearEngineHostApi(  
116 - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) 115 + WearEngineHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
117 : pigeonVar_binaryMessenger = binaryMessenger, 116 : pigeonVar_binaryMessenger = binaryMessenger,
118 - pigeonVar_messageChannelSuffix =  
119 - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; 117 + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
120 final BinaryMessenger? pigeonVar_binaryMessenger; 118 final BinaryMessenger? pigeonVar_binaryMessenger;
121 119
122 static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); 120 static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -124,10 +122,8 @@ class WearEngineHostApi { @@ -124,10 +122,8 @@ class WearEngineHostApi {
124 final String pigeonVar_messageChannelSuffix; 122 final String pigeonVar_messageChannelSuffix;
125 123
126 Future<bool> hasAvailableDevices() async { 124 Future<bool> hasAvailableDevices() async {
127 - final String pigeonVar_channelName =  
128 - 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasAvailableDevices$pigeonVar_messageChannelSuffix';  
129 - final BasicMessageChannel<Object?> pigeonVar_channel =  
130 - BasicMessageChannel<Object?>( 125 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.hasAvailableDevices$pigeonVar_messageChannelSuffix';
  126 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
131 pigeonVar_channelName, 127 pigeonVar_channelName,
132 pigeonChannelCodec, 128 pigeonChannelCodec,
133 binaryMessenger: pigeonVar_binaryMessenger, 129 binaryMessenger: pigeonVar_binaryMessenger,
@@ -154,10 +150,8 @@ class WearEngineHostApi { @@ -154,10 +150,8 @@ class WearEngineHostApi {
154 } 150 }
155 151
156 Future<WearDeviceInfo?> checkConnectedDevice() async { 152 Future<WearDeviceInfo?> checkConnectedDevice() async {
157 - final String pigeonVar_channelName =  
158 - 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.checkConnectedDevice$pigeonVar_messageChannelSuffix';  
159 - final BasicMessageChannel<Object?> pigeonVar_channel =  
160 - BasicMessageChannel<Object?>( 153 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.checkConnectedDevice$pigeonVar_messageChannelSuffix';
  154 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
161 pigeonVar_channelName, 155 pigeonVar_channelName,
162 pigeonChannelCodec, 156 pigeonChannelCodec,
163 binaryMessenger: pigeonVar_binaryMessenger, 157 binaryMessenger: pigeonVar_binaryMessenger,
@@ -179,10 +173,8 @@ class WearEngineHostApi { @@ -179,10 +173,8 @@ class WearEngineHostApi {
179 } 173 }
180 174
181 Future<bool> registerMessageReceiver() async { 175 Future<bool> registerMessageReceiver() async {
182 - final String pigeonVar_channelName =  
183 - 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.registerMessageReceiver$pigeonVar_messageChannelSuffix';  
184 - final BasicMessageChannel<Object?> pigeonVar_channel =  
185 - BasicMessageChannel<Object?>( 176 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.registerMessageReceiver$pigeonVar_messageChannelSuffix';
  177 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
186 pigeonVar_channelName, 178 pigeonVar_channelName,
187 pigeonChannelCodec, 179 pigeonChannelCodec,
188 binaryMessenger: pigeonVar_binaryMessenger, 180 binaryMessenger: pigeonVar_binaryMessenger,
@@ -209,16 +201,13 @@ class WearEngineHostApi { @@ -209,16 +201,13 @@ class WearEngineHostApi {
209 } 201 }
210 202
211 Future<bool> sendTextMessage(String message) async { 203 Future<bool> sendTextMessage(String message) async {
212 - final String pigeonVar_channelName =  
213 - 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendTextMessage$pigeonVar_messageChannelSuffix';  
214 - final BasicMessageChannel<Object?> pigeonVar_channel =  
215 - BasicMessageChannel<Object?>( 204 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendTextMessage$pigeonVar_messageChannelSuffix';
  205 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
216 pigeonVar_channelName, 206 pigeonVar_channelName,
217 pigeonChannelCodec, 207 pigeonChannelCodec,
218 binaryMessenger: pigeonVar_binaryMessenger, 208 binaryMessenger: pigeonVar_binaryMessenger,
219 ); 209 );
220 - final Future<Object?> pigeonVar_sendFuture =  
221 - pigeonVar_channel.send(<Object?>[message]); 210 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[message]);
222 final List<Object?>? pigeonVar_replyList = 211 final List<Object?>? pigeonVar_replyList =
223 await pigeonVar_sendFuture as List<Object?>?; 212 await pigeonVar_sendFuture as List<Object?>?;
224 if (pigeonVar_replyList == null) { 213 if (pigeonVar_replyList == null) {
@@ -240,16 +229,13 @@ class WearEngineHostApi { @@ -240,16 +229,13 @@ class WearEngineHostApi {
240 } 229 }
241 230
242 Future<bool> sendWatchSyncPayload(String jsonPayload) async { 231 Future<bool> sendWatchSyncPayload(String jsonPayload) async {
243 - final String pigeonVar_channelName =  
244 - 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$pigeonVar_messageChannelSuffix';  
245 - final BasicMessageChannel<Object?> pigeonVar_channel =  
246 - BasicMessageChannel<Object?>( 232 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.sendWatchSyncPayload$pigeonVar_messageChannelSuffix';
  233 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
247 pigeonVar_channelName, 234 pigeonVar_channelName,
248 pigeonChannelCodec, 235 pigeonChannelCodec,
249 binaryMessenger: pigeonVar_binaryMessenger, 236 binaryMessenger: pigeonVar_binaryMessenger,
250 ); 237 );
251 - final Future<Object?> pigeonVar_sendFuture =  
252 - pigeonVar_channel.send(<Object?>[jsonPayload]); 238 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[jsonPayload]);
253 final List<Object?>? pigeonVar_replyList = 239 final List<Object?>? pigeonVar_replyList =
254 await pigeonVar_sendFuture as List<Object?>?; 240 await pigeonVar_sendFuture as List<Object?>?;
255 if (pigeonVar_replyList == null) { 241 if (pigeonVar_replyList == null) {
@@ -270,11 +256,11 @@ class WearEngineHostApi { @@ -270,11 +256,11 @@ class WearEngineHostApi {
270 } 256 }
271 } 257 }
272 258
  259 + /// Opens the system photo picker and returns a local PNG file path after
  260 + /// removing the image background on the host platform.
273 Future<String?> pickImageAndRemoveBackground() async { 261 Future<String?> pickImageAndRemoveBackground() async {
274 - final String pigeonVar_channelName =  
275 - 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.pickImageAndRemoveBackground$pigeonVar_messageChannelSuffix';  
276 - final BasicMessageChannel<Object?> pigeonVar_channel =  
277 - BasicMessageChannel<Object?>( 262 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.WearEngineHostApi.pickImageAndRemoveBackground$pigeonVar_messageChannelSuffix';
  263 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
278 pigeonVar_channelName, 264 pigeonVar_channelName,
279 pigeonChannelCodec, 265 pigeonChannelCodec,
280 binaryMessenger: pigeonVar_binaryMessenger, 266 binaryMessenger: pigeonVar_binaryMessenger,
@@ -12,6 +12,40 @@ class HealthUploadResult { @@ -12,6 +12,40 @@ class HealthUploadResult {
12 String? errorMessage; 12 String? errorMessage;
13 } 13 }
14 14
  15 +class HealthUploadDataPoint {
  16 + HealthUploadDataPoint({
  17 + required this.dataType,
  18 + required this.time,
  19 + required this.value,
  20 + });
  21 +
  22 + int dataType;
  23 + int time;
  24 + double value;
  25 +}
  26 +
  27 +class HealthSleepUploadDataPoint {
  28 + HealthSleepUploadDataPoint({
  29 + required this.dataType,
  30 + required this.fromTime,
  31 + required this.toTime,
  32 + });
  33 +
  34 + int dataType;
  35 + int fromTime;
  36 + int toTime;
  37 +}
  38 +
  39 +class HealthActivityTargetData {
  40 + HealthActivityTargetData({
  41 + this.move,
  42 + this.stand,
  43 + });
  44 +
  45 + int? move;
  46 + int? stand;
  47 +}
  48 +
15 @ConfigurePigeon( 49 @ConfigurePigeon(
16 PigeonOptions( 50 PigeonOptions(
17 dartOut: 'lib/pigeon/health_kit_api.g.dart', 51 dartOut: 'lib/pigeon/health_kit_api.g.dart',
@@ -41,4 +75,61 @@ abstract class HealthKitHostApi { @@ -41,4 +75,61 @@ abstract class HealthKitHostApi {
41 75
42 /// Runs native health read and server upload pipeline. 76 /// Runs native health read and server upload pipeline.
43 HealthUploadResult performHealthUpload(); 77 HealthUploadResult performHealthUpload();
  78 +
  79 + List<HealthUploadDataPoint> fetchHrvData(int startTime, int endTime);
  80 +
  81 + List<HealthUploadDataPoint> fetchHeartRateData(int startTime, int endTime);
  82 +
  83 + List<HealthUploadDataPoint> fetchWalkingHeartRateData(
  84 + int startTime,
  85 + int endTime,
  86 + );
  87 +
  88 + List<HealthUploadDataPoint> fetchRestingHeartRateData(
  89 + int startTime,
  90 + int endTime,
  91 + );
  92 +
  93 + List<HealthUploadDataPoint> fetchSleepingHeartRateData(
  94 + int startTime,
  95 + int endTime,
  96 + );
  97 +
  98 + List<HealthUploadDataPoint> fetchOxygenSaturationData(
  99 + int startTime,
  100 + int endTime,
  101 + );
  102 +
  103 + List<HealthUploadDataPoint> fetchActiveEnergyData(
  104 + int startTime,
  105 + int endTime,
  106 + );
  107 +
  108 + List<HealthUploadDataPoint> fetchExerciseData(int startTime, int endTime);
  109 +
  110 + List<HealthUploadDataPoint> fetchStandData(int startTime, int endTime);
  111 +
  112 + List<HealthUploadDataPoint> fetchStepCountData(int startTime, int endTime);
  113 +
  114 + List<HealthSleepUploadDataPoint> fetchSleepData(int startTime, int endTime);
  115 +
  116 + List<HealthUploadDataPoint> fetchSleepingWristTemperatureData(
  117 + int startTime,
  118 + int endTime,
  119 + );
  120 +
  121 + List<HealthUploadDataPoint> fetchRespiratoryRateData(
  122 + int startTime,
  123 + int endTime,
  124 + );
  125 +
  126 + List<HealthUploadDataPoint> fetchIrregularHeartRhythmData(
  127 + int startTime,
  128 + int endTime,
  129 + );
  130 +
  131 + HealthActivityTargetData? fetchActivityTargetData(
  132 + int startTime,
  133 + int endTime,
  134 + );
44 } 135 }