Showing
10 changed files
with
1004 additions
and
11 deletions
| @@ -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 | } |
-
Please register or login to post a comment