Commit 4ba6cb4d7c1575e07b2e7f68492e19d5ce3ce9ed

Authored by 刘宏哲
1 parent 6f39f59d

feat(app): 添加原生目标值读取

@@ -2,6 +2,8 @@ package com.doublefeel.app.native @@ -2,6 +2,8 @@ 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.HealthActivityTargetData
  5 +import com.doublefeel.app.pigeon.HealthActivityGoal
  6 +import com.doublefeel.app.pigeon.HealthWorkoutDataPoint
5 import com.doublefeel.app.pigeon.HealthAuthorization 7 import com.doublefeel.app.pigeon.HealthAuthorization
6 import com.doublefeel.app.pigeon.HealthKitHostApi 8 import com.doublefeel.app.pigeon.HealthKitHostApi
7 import com.doublefeel.app.pigeon.HealthSleepUploadDataPoint 9 import com.doublefeel.app.pigeon.HealthSleepUploadDataPoint
@@ -38,6 +40,20 @@ class HealthKitHostApiImpl( @@ -38,6 +40,20 @@ class HealthKitHostApiImpl(
38 callback(Result.success(false)) 40 callback(Result.success(false))
39 } 41 }
40 42
  43 + override fun readActivityGoal(callback: (Result<HealthActivityGoal?>) -> Unit) {
  44 + // Activity-ring goals are read through Health Service Kit on OHOS only.
  45 + callback(Result.success(null))
  46 + }
  47 +
  48 + override fun readWorkoutData(
  49 + startTime: Long,
  50 + endTime: Long,
  51 + callback: (Result<List<HealthWorkoutDataPoint>>) -> Unit,
  52 + ) {
  53 + // Workout records are read through Health Service Kit on OHOS only.
  54 + callback(Result.success(emptyList()))
  55 + }
  56 +
41 override fun fetchHrvData( 57 override fun fetchHrvData(
42 startTime: Long, 58 startTime: Long,
43 endTime: Long, 59 endTime: Long,
@@ -64,7 +64,7 @@ private object HealthKitApiPigeonUtils { @@ -64,7 +64,7 @@ private object HealthKitApiPigeonUtils {
64 } 64 }
65 return a == b 65 return a == b
66 } 66 }
67 - 67 +
68 } 68 }
69 69
70 /** 70 /**
@@ -110,6 +110,90 @@ data class HealthAuthorization ( @@ -110,6 +110,90 @@ data class HealthAuthorization (
110 110
111 override fun hashCode(): Int = toList().hashCode() 111 override fun hashCode(): Int = toList().hashCode()
112 } 112 }
  113 +
  114 +/**
  115 + * The user's current Huawei Health activity-ring goals.
  116 + *
  117 + * Units: move is kcal, exercise is seconds, and stand is hours.
  118 + *
  119 + * Generated class from Pigeon that represents data sent in messages.
  120 + */
  121 +data class HealthActivityGoal (
  122 + val move: Long? = null,
  123 + val step: Long? = null,
  124 + val stand: Long? = null,
  125 + val exercise: Long? = null
  126 +)
  127 + {
  128 + companion object {
  129 + fun fromList(pigeonVar_list: List<Any?>): HealthActivityGoal {
  130 + val move = pigeonVar_list[0] as Long?
  131 + val step = pigeonVar_list[1] as Long?
  132 + val stand = pigeonVar_list[2] as Long?
  133 + val exercise = pigeonVar_list[3] as Long?
  134 + return HealthActivityGoal(move, step, stand, exercise)
  135 + }
  136 + }
  137 + fun toList(): List<Any?> {
  138 + return listOf(
  139 + move,
  140 + step,
  141 + stand,
  142 + exercise,
  143 + )
  144 + }
  145 + override fun equals(other: Any?): Boolean {
  146 + if (other !is HealthActivityGoal) {
  147 + return false
  148 + }
  149 + if (this === other) {
  150 + return true
  151 + }
  152 + return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
  153 +
  154 + override fun hashCode(): Int = toList().hashCode()
  155 +}
  156 +
  157 +/**
  158 + * One workout record read from Health Service Kit.
  159 + *
  160 + * Generated class from Pigeon that represents data sent in messages.
  161 + */
  162 +data class HealthWorkoutDataPoint (
  163 + /** Unix timestamp in seconds. */
  164 + val startTime: Long,
  165 + /** Unix timestamp in seconds. */
  166 + val endTime: Long,
  167 + /** The native Health Service Kit exercise-type identifier. */
  168 + val activityType: Long
  169 +)
  170 + {
  171 + companion object {
  172 + fun fromList(pigeonVar_list: List<Any?>): HealthWorkoutDataPoint {
  173 + val startTime = pigeonVar_list[0] as Long
  174 + val endTime = pigeonVar_list[1] as Long
  175 + val activityType = pigeonVar_list[2] as Long
  176 + return HealthWorkoutDataPoint(startTime, endTime, activityType)
  177 + }
  178 + }
  179 + fun toList(): List<Any?> {
  180 + return listOf(
  181 + startTime,
  182 + endTime,
  183 + activityType,
  184 + )
  185 + }
  186 + override fun equals(other: Any?): Boolean {
  187 + if (other !is HealthWorkoutDataPoint) {
  188 + return false
  189 + }
  190 + if (this === other) {
  191 + return true
  192 + }
  193 + return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
  194 +
  195 + override fun hashCode(): Int = toList().hashCode()
  196 +}
113 private open class HealthKitApiPigeonCodec : StandardMessageCodec() { 197 private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
114 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { 198 override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
115 return when (type) { 199 return when (type) {
@@ -118,6 +202,16 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() { @@ -118,6 +202,16 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
118 HealthAuthorization.fromList(it) 202 HealthAuthorization.fromList(it)
119 } 203 }
120 } 204 }
  205 + 130.toByte() -> {
  206 + return (readValue(buffer) as? List<Any?>)?.let {
  207 + HealthActivityGoal.fromList(it)
  208 + }
  209 + }
  210 + 131.toByte() -> {
  211 + return (readValue(buffer) as? List<Any?>)?.let {
  212 + HealthWorkoutDataPoint.fromList(it)
  213 + }
  214 + }
121 else -> super.readValueOfType(type, buffer) 215 else -> super.readValueOfType(type, buffer)
122 } 216 }
123 } 217 }
@@ -127,6 +221,14 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() { @@ -127,6 +221,14 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
127 stream.write(129) 221 stream.write(129)
128 writeValue(stream, value.toList()) 222 writeValue(stream, value.toList())
129 } 223 }
  224 + is HealthActivityGoal -> {
  225 + stream.write(130)
  226 + writeValue(stream, value.toList())
  227 + }
  228 + is HealthWorkoutDataPoint -> {
  229 + stream.write(131)
  230 + writeValue(stream, value.toList())
  231 + }
130 else -> super.writeValue(stream, value) 232 else -> super.writeValue(stream, value)
131 } 233 }
132 } 234 }
@@ -138,7 +240,17 @@ interface HealthKitHostApi { @@ -138,7 +240,17 @@ interface HealthKitHostApi {
138 /** Opens Huawei Health client authorization UI. Returns whether user granted. */ 240 /** Opens Huawei Health client authorization UI. Returns whether user granted. */
139 fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit) 241 fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit)
140 fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit) 242 fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit)
  243 + /**
  244 + * Requests Huawei Health to upload the user's latest data to its cloud.
  245 + *
  246 + * This only triggers the Health app's configured device-to-cloud sync; it
  247 + * does not wait for, or return, the subsequently available cloud data.
  248 + */
141 fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit) 249 fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit)
  250 + /** Reads the current activity-ring goals from Huawei Health on OHOS. */
  251 + fun readActivityGoal(callback: (Result<HealthActivityGoal?>) -> Unit)
  252 + /** Reads workout records in the supplied Unix-seconds time range on OHOS. */
  253 + fun readWorkoutData(startTime: Long, endTime: Long, callback: (Result<List<HealthWorkoutDataPoint>>) -> Unit)
142 254
143 companion object { 255 companion object {
144 /** The codec used by HealthKitHostApi. */ 256 /** The codec used by HealthKitHostApi. */
@@ -203,6 +315,45 @@ interface HealthKitHostApi { @@ -203,6 +315,45 @@ interface HealthKitHostApi {
203 channel.setMessageHandler(null) 315 channel.setMessageHandler(null)
204 } 316 }
205 } 317 }
  318 + run {
  319 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal$separatedMessageChannelSuffix", codec)
  320 + if (api != null) {
  321 + channel.setMessageHandler { _, reply ->
  322 + api.readActivityGoal{ result: Result<HealthActivityGoal?> ->
  323 + val error = result.exceptionOrNull()
  324 + if (error != null) {
  325 + reply.reply(HealthKitApiPigeonUtils.wrapError(error))
  326 + } else {
  327 + val data = result.getOrNull()
  328 + reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
  329 + }
  330 + }
  331 + }
  332 + } else {
  333 + channel.setMessageHandler(null)
  334 + }
  335 + }
  336 + run {
  337 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData$separatedMessageChannelSuffix", codec)
  338 + if (api != null) {
  339 + channel.setMessageHandler { message, reply ->
  340 + val args = message as List<Any?>
  341 + val startTimeArg = args[0] as Long
  342 + val endTimeArg = args[1] as Long
  343 + api.readWorkoutData(startTimeArg, endTimeArg) { result: Result<List<HealthWorkoutDataPoint>> ->
  344 + val error = result.exceptionOrNull()
  345 + if (error != null) {
  346 + reply.reply(HealthKitApiPigeonUtils.wrapError(error))
  347 + } else {
  348 + val data = result.getOrNull()
  349 + reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
  350 + }
  351 + }
  352 + }
  353 + } else {
  354 + channel.setMessageHandler(null)
  355 + }
  356 + }
206 } 357 }
207 } 358 }
208 } 359 }
@@ -12,6 +12,23 @@ import Foundation @@ -12,6 +12,23 @@ import Foundation
12 #error("Unsupported platform.") 12 #error("Unsupported platform.")
13 #endif 13 #endif
14 14
  15 +/// Error class for passing custom error details to Dart side.
  16 +final class PigeonError: Error {
  17 + let code: String
  18 + let message: String?
  19 + let details: Sendable?
  20 +
  21 + init(code: String, message: String?, details: Sendable?) {
  22 + self.code = code
  23 + self.message = message
  24 + self.details = details
  25 + }
  26 +
  27 + var localizedDescription: String {
  28 + return
  29 + "PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
  30 + }
  31 +}
15 32
16 private func wrapResult(_ result: Any?) -> [Any?] { 33 private func wrapResult(_ result: Any?) -> [Any?] {
17 return [result] 34 return [result]
@@ -96,7 +113,7 @@ func deepHashHealthKitApi(value: Any?, hasher: inout Hasher) { @@ -96,7 +113,7 @@ func deepHashHealthKitApi(value: Any?, hasher: inout Hasher) {
96 } 113 }
97 114
98 if let valueDict = value as? [AnyHashable: AnyHashable] { 115 if let valueDict = value as? [AnyHashable: AnyHashable] {
99 - for key in valueDict.keys { 116 + for key in valueDict.keys {
100 hasher.combine(key) 117 hasher.combine(key)
101 deepHashHealthKitApi(value: valueDict[key]!, hasher: &hasher) 118 deepHashHealthKitApi(value: valueDict[key]!, hasher: &hasher)
102 } 119 }
@@ -110,7 +127,7 @@ func deepHashHealthKitApi(value: Any?, hasher: inout Hasher) { @@ -110,7 +127,7 @@ func deepHashHealthKitApi(value: Any?, hasher: inout Hasher) {
110 return hasher.combine(String(describing: value)) 127 return hasher.combine(String(describing: value))
111 } 128 }
112 129
113 - 130 +
114 131
115 /// Generated class from Pigeon that represents data sent in messages. 132 /// Generated class from Pigeon that represents data sent in messages.
116 struct HealthAuthorization: Hashable { 133 struct HealthAuthorization: Hashable {
@@ -142,11 +159,94 @@ struct HealthAuthorization: Hashable { @@ -142,11 +159,94 @@ struct HealthAuthorization: Hashable {
142 } 159 }
143 } 160 }
144 161
  162 +/// The user's current Huawei Health activity-ring goals.
  163 +///
  164 +/// Units: move is kcal, exercise is seconds, and stand is hours.
  165 +///
  166 +/// Generated class from Pigeon that represents data sent in messages.
  167 +struct HealthActivityGoal: Hashable {
  168 + var move: Int64? = nil
  169 + var step: Int64? = nil
  170 + var stand: Int64? = nil
  171 + var exercise: Int64? = nil
  172 +
  173 +
  174 + // swift-format-ignore: AlwaysUseLowerCamelCase
  175 + static func fromList(_ pigeonVar_list: [Any?]) -> HealthActivityGoal? {
  176 + let move: Int64? = nilOrValue(pigeonVar_list[0])
  177 + let step: Int64? = nilOrValue(pigeonVar_list[1])
  178 + let stand: Int64? = nilOrValue(pigeonVar_list[2])
  179 + let exercise: Int64? = nilOrValue(pigeonVar_list[3])
  180 +
  181 + return HealthActivityGoal(
  182 + move: move,
  183 + step: step,
  184 + stand: stand,
  185 + exercise: exercise
  186 + )
  187 + }
  188 + func toList() -> [Any?] {
  189 + return [
  190 + move,
  191 + step,
  192 + stand,
  193 + exercise,
  194 + ]
  195 + }
  196 + static func == (lhs: HealthActivityGoal, rhs: HealthActivityGoal) -> Bool {
  197 + return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
  198 + func hash(into hasher: inout Hasher) {
  199 + deepHashHealthKitApi(value: toList(), hasher: &hasher)
  200 + }
  201 +}
  202 +
  203 +/// One workout record read from Health Service Kit.
  204 +///
  205 +/// Generated class from Pigeon that represents data sent in messages.
  206 +struct HealthWorkoutDataPoint: Hashable {
  207 + /// Unix timestamp in seconds.
  208 + var startTime: Int64
  209 + /// Unix timestamp in seconds.
  210 + var endTime: Int64
  211 + /// The native Health Service Kit exercise-type identifier.
  212 + var activityType: Int64
  213 +
  214 +
  215 + // swift-format-ignore: AlwaysUseLowerCamelCase
  216 + static func fromList(_ pigeonVar_list: [Any?]) -> HealthWorkoutDataPoint? {
  217 + let startTime = pigeonVar_list[0] as! Int64
  218 + let endTime = pigeonVar_list[1] as! Int64
  219 + let activityType = pigeonVar_list[2] as! Int64
  220 +
  221 + return HealthWorkoutDataPoint(
  222 + startTime: startTime,
  223 + endTime: endTime,
  224 + activityType: activityType
  225 + )
  226 + }
  227 + func toList() -> [Any?] {
  228 + return [
  229 + startTime,
  230 + endTime,
  231 + activityType,
  232 + ]
  233 + }
  234 + static func == (lhs: HealthWorkoutDataPoint, rhs: HealthWorkoutDataPoint) -> Bool {
  235 + return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
  236 + func hash(into hasher: inout Hasher) {
  237 + deepHashHealthKitApi(value: toList(), hasher: &hasher)
  238 + }
  239 +}
  240 +
145 private class HealthKitApiPigeonCodecReader: FlutterStandardReader { 241 private class HealthKitApiPigeonCodecReader: FlutterStandardReader {
146 override func readValue(ofType type: UInt8) -> Any? { 242 override func readValue(ofType type: UInt8) -> Any? {
147 switch type { 243 switch type {
148 case 129: 244 case 129:
149 return HealthAuthorization.fromList(self.readValue() as! [Any?]) 245 return HealthAuthorization.fromList(self.readValue() as! [Any?])
  246 + case 130:
  247 + return HealthActivityGoal.fromList(self.readValue() as! [Any?])
  248 + case 131:
  249 + return HealthWorkoutDataPoint.fromList(self.readValue() as! [Any?])
150 default: 250 default:
151 return super.readValue(ofType: type) 251 return super.readValue(ofType: type)
152 } 252 }
@@ -158,6 +258,12 @@ private class HealthKitApiPigeonCodecWriter: FlutterStandardWriter { @@ -158,6 +258,12 @@ private class HealthKitApiPigeonCodecWriter: FlutterStandardWriter {
158 if let value = value as? HealthAuthorization { 258 if let value = value as? HealthAuthorization {
159 super.writeByte(129) 259 super.writeByte(129)
160 super.writeValue(value.toList()) 260 super.writeValue(value.toList())
  261 + } else if let value = value as? HealthActivityGoal {
  262 + super.writeByte(130)
  263 + super.writeValue(value.toList())
  264 + } else if let value = value as? HealthWorkoutDataPoint {
  265 + super.writeByte(131)
  266 + super.writeValue(value.toList())
161 } else { 267 } else {
162 super.writeValue(value) 268 super.writeValue(value)
163 } 269 }
@@ -189,6 +295,10 @@ protocol HealthKitHostApi { @@ -189,6 +295,10 @@ protocol HealthKitHostApi {
189 /// This only triggers the Health app's configured device-to-cloud sync; it 295 /// This only triggers the Health app's configured device-to-cloud sync; it
190 /// does not wait for, or return, the subsequently available cloud data. 296 /// does not wait for, or return, the subsequently available cloud data.
191 func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void) 297 func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void)
  298 + /// Reads the current activity-ring goals from Huawei Health on OHOS.
  299 + func readActivityGoal(completion: @escaping (Result<HealthActivityGoal?, Error>) -> Void)
  300 + /// Reads workout records in the supplied Unix-seconds time range on OHOS.
  301 + func readWorkoutData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthWorkoutDataPoint], Error>) -> Void)
192 } 302 }
193 303
194 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. 304 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -247,5 +357,40 @@ class HealthKitHostApiSetup { @@ -247,5 +357,40 @@ class HealthKitHostApiSetup {
247 } else { 357 } else {
248 syncHealthDataToCloudChannel.setMessageHandler(nil) 358 syncHealthDataToCloudChannel.setMessageHandler(nil)
249 } 359 }
  360 + /// Reads the current activity-ring goals from Huawei Health on OHOS.
  361 + let readActivityGoalChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  362 + if let api = api {
  363 + readActivityGoalChannel.setMessageHandler { _, reply in
  364 + api.readActivityGoal { result in
  365 + switch result {
  366 + case .success(let res):
  367 + reply(wrapResult(res))
  368 + case .failure(let error):
  369 + reply(wrapError(error))
  370 + }
  371 + }
  372 + }
  373 + } else {
  374 + readActivityGoalChannel.setMessageHandler(nil)
  375 + }
  376 + /// Reads workout records in the supplied Unix-seconds time range on OHOS.
  377 + let readWorkoutDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  378 + if let api = api {
  379 + readWorkoutDataChannel.setMessageHandler { message, reply in
  380 + let args = message as! [Any?]
  381 + let startTimeArg = args[0] as! Int64
  382 + let endTimeArg = args[1] as! Int64
  383 + api.readWorkoutData(startTime: startTimeArg, endTime: endTimeArg) { result in
  384 + switch result {
  385 + case .success(let res):
  386 + reply(wrapResult(res))
  387 + case .failure(let error):
  388 + reply(wrapError(error))
  389 + }
  390 + }
  391 + }
  392 + } else {
  393 + readWorkoutDataChannel.setMessageHandler(nil)
  394 + }
250 } 395 }
251 } 396 }
@@ -56,4 +56,14 @@ final class HealthKitHostApiImpl: HealthKitHostApi { @@ -56,4 +56,14 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
56 // Huawei Health manual cloud synchronization is HarmonyOS-specific. 56 // Huawei Health manual cloud synchronization is HarmonyOS-specific.
57 completion(.success(false)) 57 completion(.success(false))
58 } 58 }
  59 +
  60 + func readActivityGoal(completion: @escaping (Result<HealthActivityGoal?, Error>) -> Void) {
  61 + // Activity-ring goals are read through Health Service Kit on OHOS only.
  62 + completion(.success(nil))
  63 + }
  64 +
  65 + func readWorkoutData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthWorkoutDataPoint], Error>) -> Void) {
  66 + // Workout records are read through Health Service Kit on OHOS only.
  67 + completion(.success([]))
  68 + }
59 } 69 }
@@ -223,6 +223,20 @@ class AppHealthKitHostApi extends _AppPigeonApiFacade { @@ -223,6 +223,20 @@ class AppHealthKitHostApi extends _AppPigeonApiFacade {
223 223
224 final health_kit.HealthKitHostApi _api; 224 final health_kit.HealthKitHostApi _api;
225 225
  226 + Future<health_kit.HealthActivityGoal?> readActivityGoal() => dispatch(
  227 + ios: () async => null,
  228 + ohos: () => _api.readActivityGoal(),
  229 + );
  230 +
  231 + Future<List<health_kit.HealthWorkoutDataPoint>> readWorkoutData(
  232 + int startTime,
  233 + int endTime,
  234 + ) =>
  235 + dispatch(
  236 + ios: () async => const <health_kit.HealthWorkoutDataPoint>[],
  237 + ohos: () => _api.readWorkoutData(startTime, endTime),
  238 + );
  239 +
226 Future<health_kit.HealthAuthorization> checkHealthAppAuthorization() => 240 Future<health_kit.HealthAuthorization> checkHealthAppAuthorization() =>
227 dispatch( 241 dispatch(
228 ios: () => _api.checkHealthAppAuthorization(), 242 ios: () => _api.checkHealthAppAuthorization(),
  1 +import 'package:doublefeel_flutter/core/logging/app_logger.dart';
1 import 'package:doublefeel_flutter/core/network/api/harmony_api.dart'; 2 import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
  3 +import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
2 import 'package:doublefeel_flutter/core/result/app_result.dart'; 4 import 'package:doublefeel_flutter/core/result/app_result.dart';
3 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart'; 5 import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
4 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; 6 import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
@@ -10,6 +12,13 @@ import 'huawei_health_data_type.dart'; @@ -10,6 +12,13 @@ import 'huawei_health_data_type.dart';
10 import 'ohos_health_raw_data_local_store.dart'; 12 import 'ohos_health_raw_data_local_store.dart';
11 import 'ohos_health_raw_data_sync_service.dart'; 13 import 'ohos_health_raw_data_sync_service.dart';
12 14
  15 +/// Flutter-side switch for selecting the OHOS health data source at runtime.
  16 +///
  17 +/// Set [preferNativeHealthData] to true to read Health Service Kit first.
  18 +abstract final class OhosHealthDataSourceSwitch {
  19 + static bool preferNativeHealthData = false;
  20 +}
  21 +
13 OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({ 22 OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({
14 int Function()? userIdProvider, 23 int Function()? userIdProvider,
15 }) { 24 }) {
@@ -28,9 +37,13 @@ OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({ @@ -28,9 +37,13 @@ OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({
28 } 37 }
29 38
30 class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { 39 class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
31 - const HarmonyApiOhosRawDataClient(this._harmonyApi); 40 + const HarmonyApiOhosRawDataClient(this._harmonyApi, [this._healthKitApi]);
32 41
33 final HarmonyApi _harmonyApi; 42 final HarmonyApi _harmonyApi;
  43 + final AppHealthKitHostApi? _healthKitApi;
  44 +
  45 + bool get _preferNativeHealthData =>
  46 + OhosHealthDataSourceSwitch.preferNativeHealthData;
34 47
35 @override 48 @override
36 Future<AppResult<HmHealthData>> getHealthData( 49 Future<AppResult<HmHealthData>> getHealthData(
@@ -47,12 +60,130 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient { @@ -47,12 +60,130 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
47 } 60 }
48 61
49 @override 62 @override
50 - Future<AppResult<HmWorkoutData>> getWorkoutData(int startTime, int endTime) {  
51 - return _harmonyApi.getWorkoutData(startTime, endTime); 63 + Future<AppResult<HmWorkoutData>> getWorkoutData(
  64 + int startTime,
  65 + int endTime,
  66 + ) async {
  67 + if (!_preferNativeHealthData) {
  68 + AppLogger.i(
  69 + '[OHOS_WORKOUT] source=remote native_enabled=false '
  70 + 'range=$startTime-$endTime',
  71 + );
  72 + return _harmonyApi.getWorkoutData(startTime, endTime);
  73 + }
  74 +
  75 + List<HmWorkoutDataItem> nativeItems = const <HmWorkoutDataItem>[];
  76 + try {
  77 + final records = await (_healthKitApi ?? AppHealthKitHostApi())
  78 + .readWorkoutData(startTime, endTime);
  79 + nativeItems = records
  80 + .map(
  81 + (record) => HmWorkoutDataItem(
  82 + startTime: record.startTime,
  83 + endTime: record.endTime,
  84 + activityType: record.activityType,
  85 + ),
  86 + )
  87 + .toList(growable: false);
  88 + } catch (error) {
  89 + AppLogger.i('[OHOS_WORKOUT] native_failed error=$error');
  90 + }
  91 +
  92 + if (nativeItems.isNotEmpty) {
  93 + AppLogger.i(
  94 + '[OHOS_WORKOUT] source=native '
  95 + 'range=$startTime-$endTime '
  96 + 'value=${_workoutLogValue(nativeItems)}',
  97 + );
  98 + return AppSuccess(HmWorkoutData(list: nativeItems));
  99 + }
  100 +
  101 + final remoteResult = await _harmonyApi.getWorkoutData(startTime, endTime);
  102 + switch (remoteResult) {
  103 + case AppSuccess(:final data):
  104 + AppLogger.i(
  105 + '[OHOS_WORKOUT] '
  106 + 'range=$startTime-$endTime '
  107 + 'source=remote value=${_workoutLogValue(data.list ?? const <HmWorkoutDataItem>[])}',
  108 + );
  109 + case AppFailure(:final error):
  110 + AppLogger.i(
  111 + '[OHOS_WORKOUT] '
  112 + 'range=$startTime-$endTime source=remote_failed error=$error',
  113 + );
  114 + }
  115 +
  116 + return remoteResult;
52 } 117 }
53 118
54 @override 119 @override
55 - Future<AppResult<V2ActivityTarget>> getActivityGoal() {  
56 - return _harmonyApi.getActivityGoal(); 120 + Future<AppResult<V2ActivityTarget>> getActivityGoal() async {
  121 + if (!_preferNativeHealthData) {
  122 + AppLogger.i('[OHOS_ACTIVITY_GOAL] source=remote native_enabled=false');
  123 + return _harmonyApi.getActivityGoal();
  124 + }
  125 +
  126 + try {
  127 + final goal =
  128 + await (_healthKitApi ?? AppHealthKitHostApi()).readActivityGoal();
  129 + if (goal != null) {
  130 + final nativeGoal = V2ActivityTarget(
  131 + move: goal.move,
  132 + step: goal.step,
  133 + stand: goal.stand,
  134 + exercise: goal.exercise,
  135 + );
  136 + if (_hasGoal(nativeGoal)) {
  137 + AppLogger.i(
  138 + '[OHOS_ACTIVITY_GOAL] '
  139 + 'source=native value=${_goalLogValue(nativeGoal)}',
  140 + );
  141 + return AppSuccess(nativeGoal);
  142 + }
  143 + }
  144 + } catch (error) {
  145 + // Health Service Kit may be unavailable or not authorized. Keep the
  146 + // existing server endpoint as the compatibility fallback.
  147 + AppLogger.i('[OHOS_ACTIVITY_GOAL] native_failed error=$error');
  148 + }
  149 +
  150 + final remoteResult = await _harmonyApi.getActivityGoal();
  151 + switch (remoteResult) {
  152 + case AppSuccess(:final data):
  153 + AppLogger.i(
  154 + '[OHOS_ACTIVITY_GOAL] '
  155 + 'source=remote value=${_goalLogValue(data)}',
  156 + );
  157 + case AppFailure(:final error):
  158 + AppLogger.i(
  159 + '[OHOS_ACTIVITY_GOAL] source=remote_failed error=$error',
  160 + );
  161 + }
  162 +
  163 + return remoteResult;
  164 + }
  165 +
  166 + static bool _hasGoal(V2ActivityTarget goal) {
  167 + return goal.move != null ||
  168 + goal.step != null ||
  169 + goal.stand != null ||
  170 + goal.exercise != null;
  171 + }
  172 +
  173 + static String _goalLogValue(V2ActivityTarget? goal) {
  174 + if (goal == null) return 'null';
  175 + return 'move=${goal.move},step=${goal.step},stand=${goal.stand},exercise=${goal.exercise}';
  176 + }
  177 +
  178 + static String _workoutLogValue(List<HmWorkoutDataItem> items) {
  179 + const logLimit = 100;
  180 + final values = items
  181 + .take(logLimit)
  182 + .map(
  183 + (item) => '${item.startTime}-${item.endTime}-${item.activityType}',
  184 + )
  185 + .join(',');
  186 + final suffix = items.length > logLimit ? ',…' : '';
  187 + return 'count=${items.length} [$values$suffix]';
57 } 188 }
58 } 189 }
@@ -77,6 +77,120 @@ class HealthAuthorization { @@ -77,6 +77,120 @@ class HealthAuthorization {
77 ; 77 ;
78 } 78 }
79 79
  80 +/// The user's current Huawei Health activity-ring goals.
  81 +///
  82 +/// Units: move is kcal, exercise is seconds, and stand is hours.
  83 +class HealthActivityGoal {
  84 + HealthActivityGoal({
  85 + this.move,
  86 + this.step,
  87 + this.stand,
  88 + this.exercise,
  89 + });
  90 +
  91 + int? move;
  92 +
  93 + int? step;
  94 +
  95 + int? stand;
  96 +
  97 + int? exercise;
  98 +
  99 + List<Object?> _toList() {
  100 + return <Object?>[
  101 + move,
  102 + step,
  103 + stand,
  104 + exercise,
  105 + ];
  106 + }
  107 +
  108 + Object encode() {
  109 + return _toList(); }
  110 +
  111 + static HealthActivityGoal decode(Object result) {
  112 + result as List<Object?>;
  113 + return HealthActivityGoal(
  114 + move: result[0] as int?,
  115 + step: result[1] as int?,
  116 + stand: result[2] as int?,
  117 + exercise: result[3] as int?,
  118 + );
  119 + }
  120 +
  121 + @override
  122 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  123 + bool operator ==(Object other) {
  124 + if (other is! HealthActivityGoal || other.runtimeType != runtimeType) {
  125 + return false;
  126 + }
  127 + if (identical(this, other)) {
  128 + return true;
  129 + }
  130 + return _deepEquals(encode(), other.encode());
  131 + }
  132 +
  133 + @override
  134 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  135 + int get hashCode => Object.hashAll(_toList())
  136 +;
  137 +}
  138 +
  139 +/// One workout record read from Health Service Kit.
  140 +class HealthWorkoutDataPoint {
  141 + HealthWorkoutDataPoint({
  142 + required this.startTime,
  143 + required this.endTime,
  144 + required this.activityType,
  145 + });
  146 +
  147 + /// Unix timestamp in seconds.
  148 + int startTime;
  149 +
  150 + /// Unix timestamp in seconds.
  151 + int endTime;
  152 +
  153 + /// The native Health Service Kit exercise-type identifier.
  154 + int activityType;
  155 +
  156 + List<Object?> _toList() {
  157 + return <Object?>[
  158 + startTime,
  159 + endTime,
  160 + activityType,
  161 + ];
  162 + }
  163 +
  164 + Object encode() {
  165 + return _toList(); }
  166 +
  167 + static HealthWorkoutDataPoint decode(Object result) {
  168 + result as List<Object?>;
  169 + return HealthWorkoutDataPoint(
  170 + startTime: result[0]! as int,
  171 + endTime: result[1]! as int,
  172 + activityType: result[2]! as int,
  173 + );
  174 + }
  175 +
  176 + @override
  177 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  178 + bool operator ==(Object other) {
  179 + if (other is! HealthWorkoutDataPoint || other.runtimeType != runtimeType) {
  180 + return false;
  181 + }
  182 + if (identical(this, other)) {
  183 + return true;
  184 + }
  185 + return _deepEquals(encode(), other.encode());
  186 + }
  187 +
  188 + @override
  189 + // ignore: avoid_equals_and_hash_code_on_mutable_classes
  190 + int get hashCode => Object.hashAll(_toList())
  191 +;
  192 +}
  193 +
80 194
81 class _PigeonCodec extends StandardMessageCodec { 195 class _PigeonCodec extends StandardMessageCodec {
82 const _PigeonCodec(); 196 const _PigeonCodec();
@@ -88,6 +202,12 @@ class _PigeonCodec extends StandardMessageCodec { @@ -88,6 +202,12 @@ class _PigeonCodec extends StandardMessageCodec {
88 } else if (value is HealthAuthorization) { 202 } else if (value is HealthAuthorization) {
89 buffer.putUint8(129); 203 buffer.putUint8(129);
90 writeValue(buffer, value.encode()); 204 writeValue(buffer, value.encode());
  205 + } else if (value is HealthActivityGoal) {
  206 + buffer.putUint8(130);
  207 + writeValue(buffer, value.encode());
  208 + } else if (value is HealthWorkoutDataPoint) {
  209 + buffer.putUint8(131);
  210 + writeValue(buffer, value.encode());
91 } else { 211 } else {
92 super.writeValue(buffer, value); 212 super.writeValue(buffer, value);
93 } 213 }
@@ -96,8 +216,12 @@ class _PigeonCodec extends StandardMessageCodec { @@ -96,8 +216,12 @@ class _PigeonCodec extends StandardMessageCodec {
96 @override 216 @override
97 Object? readValueOfType(int type, ReadBuffer buffer) { 217 Object? readValueOfType(int type, ReadBuffer buffer) {
98 switch (type) { 218 switch (type) {
99 - case 129: 219 + case 129:
100 return HealthAuthorization.decode(readValue(buffer)!); 220 return HealthAuthorization.decode(readValue(buffer)!);
  221 + case 130:
  222 + return HealthActivityGoal.decode(readValue(buffer)!);
  223 + case 131:
  224 + return HealthWorkoutDataPoint.decode(readValue(buffer)!);
101 default: 225 default:
102 return super.readValueOfType(type, buffer); 226 return super.readValueOfType(type, buffer);
103 } 227 }
@@ -205,4 +329,57 @@ class HealthKitHostApi { @@ -205,4 +329,57 @@ class HealthKitHostApi {
205 return (pigeonVar_replyList[0] as bool?)!; 329 return (pigeonVar_replyList[0] as bool?)!;
206 } 330 }
207 } 331 }
  332 +
  333 + /// Reads the current activity-ring goals from Huawei Health on OHOS.
  334 + Future<HealthActivityGoal?> readActivityGoal() async {
  335 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal$pigeonVar_messageChannelSuffix';
  336 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  337 + pigeonVar_channelName,
  338 + pigeonChannelCodec,
  339 + binaryMessenger: pigeonVar_binaryMessenger,
  340 + );
  341 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
  342 + final List<Object?>? pigeonVar_replyList =
  343 + await pigeonVar_sendFuture as List<Object?>?;
  344 + if (pigeonVar_replyList == null) {
  345 + throw _createConnectionError(pigeonVar_channelName);
  346 + } else if (pigeonVar_replyList.length > 1) {
  347 + throw PlatformException(
  348 + code: pigeonVar_replyList[0]! as String,
  349 + message: pigeonVar_replyList[1] as String?,
  350 + details: pigeonVar_replyList[2],
  351 + );
  352 + } else {
  353 + return (pigeonVar_replyList[0] as HealthActivityGoal?);
  354 + }
  355 + }
  356 +
  357 + /// Reads workout records in the supplied Unix-seconds time range on OHOS.
  358 + Future<List<HealthWorkoutDataPoint>> readWorkoutData(int startTime, int endTime) async {
  359 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData$pigeonVar_messageChannelSuffix';
  360 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  361 + pigeonVar_channelName,
  362 + pigeonChannelCodec,
  363 + binaryMessenger: pigeonVar_binaryMessenger,
  364 + );
  365 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[startTime, endTime]);
  366 + final List<Object?>? pigeonVar_replyList =
  367 + await pigeonVar_sendFuture as List<Object?>?;
  368 + if (pigeonVar_replyList == null) {
  369 + throw _createConnectionError(pigeonVar_channelName);
  370 + } else if (pigeonVar_replyList.length > 1) {
  371 + throw PlatformException(
  372 + code: pigeonVar_replyList[0]! as String,
  373 + message: pigeonVar_replyList[1] as String?,
  374 + details: pigeonVar_replyList[2],
  375 + );
  376 + } else if (pigeonVar_replyList[0] == null) {
  377 + throw PlatformException(
  378 + code: 'null-error',
  379 + message: 'Host platform returned null value for non-null return value.',
  380 + );
  381 + } else {
  382 + return (pigeonVar_replyList[0] as List<Object?>?)!.cast<HealthWorkoutDataPoint>();
  383 + }
  384 + }
208 } 385 }
1 -import { HealthAuthorization, HealthKitHostApi, Result } from '../pigeon/HealthKitApi'; 1 +import { HealthActivityGoal, HealthAuthorization, HealthKitHostApi, HealthWorkoutDataPoint, Result } from '../pigeon/HealthKitApi';
2 import common from '@ohos.app.ability.common'; 2 import common from '@ohos.app.ability.common';
3 import { bundleManager, Want } from '@kit.AbilityKit'; 3 import { bundleManager, Want } from '@kit.AbilityKit';
4 import { productViewManager } from '@kit.AppGalleryKit'; 4 import { productViewManager } from '@kit.AppGalleryKit';
5 import { BusinessError } from '@kit.BasicServicesKit'; 5 import { BusinessError } from '@kit.BasicServicesKit';
6 -import { healthStore } from '@kit.HealthServiceKit'; 6 +import { healthService, healthStore } from '@kit.HealthServiceKit';
7 7
8 /** 8 /**
9 * Bridges Flutter's health authorization flow to Health Service Kit. 9 * Bridges Flutter's health authorization flow to Health Service Kit.
@@ -91,11 +91,62 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { @@ -91,11 +91,62 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
91 }); 91 });
92 } 92 }
93 93
  94 + readActivityGoal(result: Result<HealthActivityGoal | undefined>): void {
  95 + this.ensureInitialized()
  96 + .then(() => healthService.workout.readActivityReport())
  97 + .then((report: healthService.workout.ActivityReport) => {
  98 + result.success(new HealthActivityGoal(
  99 + this.activeCaloriesToMoveGoal(report.activeCaloriesGoal),
  100 + report.stepsGoal,
  101 + report.activeHoursGoal,
  102 + this.exerciseMinutesToSeconds(report.exerciseGoal),
  103 + ));
  104 + })
  105 + .catch((error: Error) => {
  106 + console.error(`readActivityGoal failed: ${error.name}: ${error.message}`);
  107 + result.error(error);
  108 + });
  109 + }
  110 +
  111 + readWorkoutData(
  112 + startTime: number,
  113 + endTime: number,
  114 + result: Result<Array<HealthWorkoutDataPoint>>,
  115 + ): void {
  116 + this.ensureInitialized()
  117 + .then(() => healthStore.readData<healthStore.ExerciseSequence>({
  118 + startTime: startTime * 1000,
  119 + endTime: endTime * 1000,
  120 + exerciseType: null,
  121 + }))
  122 + .then((records: Array<healthStore.ExerciseSequence>) => {
  123 + result.success(records.map((record: healthStore.ExerciseSequence) => {
  124 + return new HealthWorkoutDataPoint(
  125 + Math.floor(record.startTime / 1000),
  126 + Math.floor(record.endTime / 1000),
  127 + record.exerciseType.id,
  128 + );
  129 + }));
  130 + })
  131 + .catch((error: Error) => {
  132 + console.error(`readWorkoutData failed: ${error.name}: ${error.message}`);
  133 + result.error(error);
  134 + });
  135 + }
  136 +
94 private async getAuthorization(): Promise<healthStore.AuthorizationResponse> { 137 private async getAuthorization(): Promise<healthStore.AuthorizationResponse> {
95 await this.ensureInitialized(); 138 await this.ensureInitialized();
96 return healthStore.getAuthorizations(this.authorizationRequest); 139 return healthStore.getAuthorizations(this.authorizationRequest);
97 } 140 }
98 141
  142 + private exerciseMinutesToSeconds(value: number | undefined): number | undefined {
  143 + return value === undefined ? undefined : Math.round(value * 60);
  144 + }
  145 +
  146 + private activeCaloriesToMoveGoal(value: number | undefined): number | undefined {
  147 + return value === undefined ? undefined : Math.floor(value / 1000);
  148 + }
  149 +
99 private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> { 150 private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> {
100 await this.ensureInitialized(); 151 await this.ensureInitialized();
101 return healthStore.requestAuthorizations(this.context, this.authorizationRequest); 152 return healthStore.requestAuthorizations(this.context, this.authorizationRequest);
@@ -96,6 +96,184 @@ export class HealthAuthorization { @@ -96,6 +96,184 @@ export class HealthAuthorization {
96 } 96 }
97 } 97 }
98 98
  99 +/*
  100 +* The user's current Huawei Health activity-ring goals.
  101 +*
  102 +* Units: move is kcal, exercise is seconds, and stand is hours.
  103 +*
  104 +* Generated class from Pigeon that represents data sent in messages.
  105 +*/
  106 +export class HealthActivityGoal {
  107 + private move?: number;
  108 +
  109 + public setMove(move: number | undefined): void {
  110 + this.move = move;
  111 + }
  112 +
  113 + getMove(): number | undefined {
  114 + return this.move;
  115 + }
  116 +
  117 + private step?: number;
  118 +
  119 + public setStep(step: number | undefined): void {
  120 + this.step = step;
  121 + }
  122 +
  123 + getStep(): number | undefined {
  124 + return this.step;
  125 + }
  126 +
  127 + private stand?: number;
  128 +
  129 + public setStand(stand: number | undefined): void {
  130 + this.stand = stand;
  131 + }
  132 +
  133 + getStand(): number | undefined {
  134 + return this.stand;
  135 + }
  136 +
  137 + private exercise?: number;
  138 +
  139 + public setExercise(exercise: number | undefined): void {
  140 + this.exercise = exercise;
  141 + }
  142 +
  143 + getExercise(): number | undefined {
  144 + return this.exercise;
  145 + }
  146 +
  147 + constructor(move?: number, step?: number, stand?: number, exercise?: number) {
  148 + this.move = move;
  149 + this.step = step;
  150 + this.stand = stand;
  151 + this.exercise = exercise;
  152 + }
  153 +
  154 + toList(): Array<Object | null> {
  155 + let arr: Array<Object | null> = new Array<Object | null>();
  156 + if (this.move === undefined || this.move === null) {
  157 + arr.push(null);
  158 + } else {
  159 + arr.push(this.move);
  160 + }
  161 + if (this.step === undefined || this.step === null) {
  162 + arr.push(null);
  163 + } else {
  164 + arr.push(this.step);
  165 + }
  166 + if (this.stand === undefined || this.stand === null) {
  167 + arr.push(null);
  168 + } else {
  169 + arr.push(this.stand);
  170 + }
  171 + if (this.exercise === undefined || this.exercise === null) {
  172 + arr.push(null);
  173 + } else {
  174 + arr.push(this.exercise);
  175 + }
  176 + return arr;
  177 + }
  178 +
  179 + static fromList(arr: Object[]): HealthActivityGoal {
  180 + let move: number | undefined = undefined;
  181 + if (arr[0] !== null && arr[0] !== undefined) {
  182 + let moveObject: Object = arr[0];
  183 + move = moveObject as number | undefined;
  184 + }
  185 + let step: number | undefined = undefined;
  186 + if (arr[1] !== null && arr[1] !== undefined) {
  187 + let stepObject: Object = arr[1];
  188 + step = stepObject as number | undefined;
  189 + }
  190 + let stand: number | undefined = undefined;
  191 + if (arr[2] !== null && arr[2] !== undefined) {
  192 + let standObject: Object = arr[2];
  193 + stand = standObject as number | undefined;
  194 + }
  195 + let exercise: number | undefined = undefined;
  196 + if (arr[3] !== null && arr[3] !== undefined) {
  197 + let exerciseObject: Object = arr[3];
  198 + exercise = exerciseObject as number | undefined;
  199 + }
  200 + return new HealthActivityGoal(move, step, stand, exercise);
  201 + }
  202 +}
  203 +
  204 +/*
  205 +* One workout record read from Health Service Kit.
  206 +*
  207 +* Generated class from Pigeon that represents data sent in messages.
  208 +*/
  209 +export class HealthWorkoutDataPoint {
  210 + private startTime: number;
  211 +
  212 + public setStartTime(startTime: number): void {
  213 + this.startTime = startTime;
  214 + }
  215 +
  216 + getStartTime(): number {
  217 + return this.startTime;
  218 + }
  219 +
  220 + private endTime: number;
  221 +
  222 + public setEndTime(endTime: number): void {
  223 + this.endTime = endTime;
  224 + }
  225 +
  226 + getEndTime(): number {
  227 + return this.endTime;
  228 + }
  229 +
  230 + private activityType: number;
  231 +
  232 + public setActivityType(activityType: number): void {
  233 + this.activityType = activityType;
  234 + }
  235 +
  236 + getActivityType(): number {
  237 + return this.activityType;
  238 + }
  239 +
  240 + constructor(startTime: number, endTime: number, activityType: number) {
  241 + this.startTime = startTime;
  242 + this.endTime = endTime;
  243 + this.activityType = activityType;
  244 + }
  245 +
  246 + toList(): Array<Object | null> {
  247 + let arr: Array<Object | null> = new Array<Object | null>();
  248 + if (this.startTime === undefined || this.startTime === null) {
  249 + arr.push(null);
  250 + } else {
  251 + arr.push(this.startTime);
  252 + }
  253 + if (this.endTime === undefined || this.endTime === null) {
  254 + arr.push(null);
  255 + } else {
  256 + arr.push(this.endTime);
  257 + }
  258 + if (this.activityType === undefined || this.activityType === null) {
  259 + arr.push(null);
  260 + } else {
  261 + arr.push(this.activityType);
  262 + }
  263 + return arr;
  264 + }
  265 +
  266 + static fromList(arr: Object[]): HealthWorkoutDataPoint {
  267 + let startTimeObject: Object = arr[0];
  268 + const startTime: number = startTimeObject as number;
  269 + let endTimeObject: Object = arr[1];
  270 + const endTime: number = endTimeObject as number;
  271 + let activityTypeObject: Object = arr[2];
  272 + const activityType: number = activityTypeObject as number;
  273 + return new HealthWorkoutDataPoint(startTime, endTime, activityType);
  274 + }
  275 +}
  276 +
99 export class PigeonCodec extends StandardMessageCodec { 277 export class PigeonCodec extends StandardMessageCodec {
100 static readonly INSTANCE: PigeonCodec = new PigeonCodec(); 278 static readonly INSTANCE: PigeonCodec = new PigeonCodec();
101 279
@@ -112,6 +290,10 @@ export class PigeonCodec extends StandardMessageCodec { @@ -112,6 +290,10 @@ export class PigeonCodec extends StandardMessageCodec {
112 switch (type) { 290 switch (type) {
113 case this.getByte(129): 291 case this.getByte(129):
114 return HealthAuthorization.fromList(super.readValue(buffer) as Object[]); 292 return HealthAuthorization.fromList(super.readValue(buffer) as Object[]);
  293 + case this.getByte(130):
  294 + return HealthActivityGoal.fromList(super.readValue(buffer) as Object[]);
  295 + case this.getByte(131):
  296 + return HealthWorkoutDataPoint.fromList(super.readValue(buffer) as Object[]);
115 default: 297 default:
116 return super.readValueOfType(type, buffer); 298 return super.readValueOfType(type, buffer);
117 } 299 }
@@ -121,6 +303,12 @@ export class PigeonCodec extends StandardMessageCodec { @@ -121,6 +303,12 @@ export class PigeonCodec extends StandardMessageCodec {
121 if (value instanceof HealthAuthorization) { 303 if (value instanceof HealthAuthorization) {
122 stream.writeUint8(this.getByte(129)); 304 stream.writeUint8(this.getByte(129));
123 this.writeValue(stream, (value as HealthAuthorization).toList()); 305 this.writeValue(stream, (value as HealthAuthorization).toList());
  306 + } else if (value instanceof HealthActivityGoal) {
  307 + stream.writeUint8(this.getByte(130));
  308 + this.writeValue(stream, (value as HealthActivityGoal).toList());
  309 + } else if (value instanceof HealthWorkoutDataPoint) {
  310 + stream.writeUint8(this.getByte(131));
  311 + this.writeValue(stream, (value as HealthWorkoutDataPoint).toList());
124 } else { 312 } else {
125 super.writeValue(stream, value); 313 super.writeValue(stream, value);
126 } 314 }
@@ -146,6 +334,10 @@ export abstract class HealthKitHostApi { @@ -146,6 +334,10 @@ export abstract class HealthKitHostApi {
146 * does not wait for, or return, the subsequently available cloud data. 334 * does not wait for, or return, the subsequently available cloud data.
147 */ 335 */
148 abstract syncHealthDataToCloud(result: Result<boolean>): void; 336 abstract syncHealthDataToCloud(result: Result<boolean>): void;
  337 + /* Reads the current activity-ring goals from Huawei Health on OHOS.*/
  338 + abstract readActivityGoal(result: Result<HealthActivityGoal | undefined>): void;
  339 + /* Reads workout records in the supplied Unix-seconds time range on OHOS.*/
  340 + abstract readWorkoutData(startTime: number, endTime: number, result: Result<Array<HealthWorkoutDataPoint>>): void;
149 /** The codec used by HealthKitHostApi. */ 341 /** The codec used by HealthKitHostApi. */
150 static getCodec(): MessageCodec<Object> { 342 static getCodec(): MessageCodec<Object> {
151 return PigeonCodec.INSTANCE; 343 return PigeonCodec.INSTANCE;
@@ -234,5 +426,68 @@ export abstract class HealthKitHostApi { @@ -234,5 +426,68 @@ export abstract class HealthKitHostApi {
234 channel.setMessageHandler(null); 426 channel.setMessageHandler(null);
235 } 427 }
236 } 428 }
  429 + {
  430 + let channel: BasicMessageChannel<Object> =
  431 + new BasicMessageChannel(
  432 + binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
  433 + if (api != null) {
  434 + channel.setMessageHandler({
  435 + onMessage(message: Object, reply: Reply<Object>) {
  436 + class ResultImp implements Result<HealthActivityGoal | undefined>{
  437 + success(result: HealthActivityGoal | undefined): void {
  438 + let res: Array<Object | null> = [];
  439 + res.push(result);
  440 + reply.reply(res);
  441 + }
  442 +
  443 + error(error: Error): void {
  444 + let wrappedError: Array<Object | null> = wrapError(error);
  445 + reply.reply(wrappedError);
  446 + }
  447 + }
  448 + let resultCallback: Result<HealthActivityGoal | undefined> = new ResultImp();
  449 +
  450 + api!.readActivityGoal(resultCallback);
  451 + } });
  452 + } else {
  453 + channel.setMessageHandler(null);
  454 + }
  455 + }
  456 + {
  457 + let channel: BasicMessageChannel<Object> =
  458 + new BasicMessageChannel(
  459 + binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
  460 + if (api != null) {
  461 + channel.setMessageHandler({
  462 + onMessage(message: Object, reply: Reply<Object>) {
  463 + if (!(Array.isArray(message))) {
  464 + reply.reply(wrapError(new Error('Invalid Pigeon message: expected List.')));
  465 + return;
  466 + }
  467 + let args: Array<Object> = message as Array<Object>;
  468 + if (args.length < 2) {
  469 + reply.reply(wrapError(new Error('Invalid Pigeon message: expected at least 2 argument(s).')));
  470 + return;
  471 + }
  472 + class ResultImp implements Result<Array<HealthWorkoutDataPoint>>{
  473 + success(result: Array<HealthWorkoutDataPoint>): void {
  474 + let res: Array<Object | null> = [];
  475 + res.push(result);
  476 + reply.reply(res);
  477 + }
  478 +
  479 + error(error: Error): void {
  480 + let wrappedError: Array<Object | null> = wrapError(error);
  481 + reply.reply(wrappedError);
  482 + }
  483 + }
  484 + let resultCallback: Result<Array<HealthWorkoutDataPoint>> = new ResultImp();
  485 +
  486 + api!.readWorkoutData(args[0] as number, args[1] as number, resultCallback);
  487 + } });
  488 + } else {
  489 + channel.setMessageHandler(null);
  490 + }
  491 + }
237 } 492 }
238 } 493 }
@@ -8,6 +8,41 @@ class HealthAuthorization { @@ -8,6 +8,41 @@ class HealthAuthorization {
8 HealthAuthorization({required this.status, required this.hasData}); 8 HealthAuthorization({required this.status, required this.hasData});
9 } 9 }
10 10
  11 +/// The user's current Huawei Health activity-ring goals.
  12 +///
  13 +/// Units: move is kcal, exercise is seconds, and stand is hours.
  14 +class HealthActivityGoal {
  15 + HealthActivityGoal({
  16 + this.move,
  17 + this.step,
  18 + this.stand,
  19 + this.exercise,
  20 + });
  21 +
  22 + int? move;
  23 + int? step;
  24 + int? stand;
  25 + int? exercise;
  26 +}
  27 +
  28 +/// One workout record read from Health Service Kit.
  29 +class HealthWorkoutDataPoint {
  30 + HealthWorkoutDataPoint({
  31 + required this.startTime,
  32 + required this.endTime,
  33 + required this.activityType,
  34 + });
  35 +
  36 + /// Unix timestamp in seconds.
  37 + int startTime;
  38 +
  39 + /// Unix timestamp in seconds.
  40 + int endTime;
  41 +
  42 + /// The native Health Service Kit exercise-type identifier.
  43 + int activityType;
  44 +}
  45 +
11 @ConfigurePigeon( 46 @ConfigurePigeon(
12 PigeonOptions( 47 PigeonOptions(
13 dartOut: 'lib/pigeon/health_kit_api.g.dart', 48 dartOut: 'lib/pigeon/health_kit_api.g.dart',
@@ -39,4 +74,12 @@ abstract class HealthKitHostApi { @@ -39,4 +74,12 @@ abstract class HealthKitHostApi {
39 /// does not wait for, or return, the subsequently available cloud data. 74 /// does not wait for, or return, the subsequently available cloud data.
40 @async 75 @async
41 bool syncHealthDataToCloud(); 76 bool syncHealthDataToCloud();
  77 +
  78 + /// Reads the current activity-ring goals from Huawei Health on OHOS.
  79 + @async
  80 + HealthActivityGoal? readActivityGoal();
  81 +
  82 + /// Reads workout records in the supplied Unix-seconds time range on OHOS.
  83 + @async
  84 + List<HealthWorkoutDataPoint> readWorkoutData(int startTime, int endTime);
42 } 85 }