Commit 4ba6cb4d7c1575e07b2e7f68492e19d5ce3ce9ed

Authored by 刘宏哲
1 parent 6f39f59d

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

... ... @@ -2,6 +2,8 @@ package com.doublefeel.app.native
import com.doublefeel.app.MainActivity
import com.doublefeel.app.pigeon.HealthActivityTargetData
import com.doublefeel.app.pigeon.HealthActivityGoal
import com.doublefeel.app.pigeon.HealthWorkoutDataPoint
import com.doublefeel.app.pigeon.HealthAuthorization
import com.doublefeel.app.pigeon.HealthKitHostApi
import com.doublefeel.app.pigeon.HealthSleepUploadDataPoint
... ... @@ -38,6 +40,20 @@ class HealthKitHostApiImpl(
callback(Result.success(false))
}
override fun readActivityGoal(callback: (Result<HealthActivityGoal?>) -> Unit) {
// Activity-ring goals are read through Health Service Kit on OHOS only.
callback(Result.success(null))
}
override fun readWorkoutData(
startTime: Long,
endTime: Long,
callback: (Result<List<HealthWorkoutDataPoint>>) -> Unit,
) {
// Workout records are read through Health Service Kit on OHOS only.
callback(Result.success(emptyList()))
}
override fun fetchHrvData(
startTime: Long,
endTime: Long,
... ...
... ... @@ -64,7 +64,7 @@ private object HealthKitApiPigeonUtils {
}
return a == b
}
}
/**
... ... @@ -110,6 +110,90 @@ data class HealthAuthorization (
override fun hashCode(): Int = toList().hashCode()
}
/**
* The user's current Huawei Health activity-ring goals.
*
* Units: move is kcal, exercise is seconds, and stand is hours.
*
* Generated class from Pigeon that represents data sent in messages.
*/
data class HealthActivityGoal (
val move: Long? = null,
val step: Long? = null,
val stand: Long? = null,
val exercise: Long? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): HealthActivityGoal {
val move = pigeonVar_list[0] as Long?
val step = pigeonVar_list[1] as Long?
val stand = pigeonVar_list[2] as Long?
val exercise = pigeonVar_list[3] as Long?
return HealthActivityGoal(move, step, stand, exercise)
}
}
fun toList(): List<Any?> {
return listOf(
move,
step,
stand,
exercise,
)
}
override fun equals(other: Any?): Boolean {
if (other !is HealthActivityGoal) {
return false
}
if (this === other) {
return true
}
return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
/**
* One workout record read from Health Service Kit.
*
* Generated class from Pigeon that represents data sent in messages.
*/
data class HealthWorkoutDataPoint (
/** Unix timestamp in seconds. */
val startTime: Long,
/** Unix timestamp in seconds. */
val endTime: Long,
/** The native Health Service Kit exercise-type identifier. */
val activityType: Long
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): HealthWorkoutDataPoint {
val startTime = pigeonVar_list[0] as Long
val endTime = pigeonVar_list[1] as Long
val activityType = pigeonVar_list[2] as Long
return HealthWorkoutDataPoint(startTime, endTime, activityType)
}
}
fun toList(): List<Any?> {
return listOf(
startTime,
endTime,
activityType,
)
}
override fun equals(other: Any?): Boolean {
if (other !is HealthWorkoutDataPoint) {
return false
}
if (this === other) {
return true
}
return HealthKitApiPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
... ... @@ -118,6 +202,16 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
HealthAuthorization.fromList(it)
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HealthActivityGoal.fromList(it)
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HealthWorkoutDataPoint.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
... ... @@ -127,6 +221,14 @@ private open class HealthKitApiPigeonCodec : StandardMessageCodec() {
stream.write(129)
writeValue(stream, value.toList())
}
is HealthActivityGoal -> {
stream.write(130)
writeValue(stream, value.toList())
}
is HealthWorkoutDataPoint -> {
stream.write(131)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
... ... @@ -138,7 +240,17 @@ interface HealthKitHostApi {
/** Opens Huawei Health client authorization UI. Returns whether user granted. */
fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit)
fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit)
/**
* Requests Huawei Health to upload the user's latest data to its cloud.
*
* This only triggers the Health app's configured device-to-cloud sync; it
* does not wait for, or return, the subsequently available cloud data.
*/
fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit)
/** Reads the current activity-ring goals from Huawei Health on OHOS. */
fun readActivityGoal(callback: (Result<HealthActivityGoal?>) -> Unit)
/** Reads workout records in the supplied Unix-seconds time range on OHOS. */
fun readWorkoutData(startTime: Long, endTime: Long, callback: (Result<List<HealthWorkoutDataPoint>>) -> Unit)
companion object {
/** The codec used by HealthKitHostApi. */
... ... @@ -203,6 +315,45 @@ interface HealthKitHostApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.readActivityGoal{ result: Result<HealthActivityGoal?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(HealthKitApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val startTimeArg = args[0] as Long
val endTimeArg = args[1] as Long
api.readWorkoutData(startTimeArg, endTimeArg) { result: Result<List<HealthWorkoutDataPoint>> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(HealthKitApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
... ...
... ... @@ -12,6 +12,23 @@ import Foundation
#error("Unsupported platform.")
#endif
/// Error class for passing custom error details to Dart side.
final class PigeonError: Error {
let code: String
let message: String?
let details: Sendable?
init(code: String, message: String?, details: Sendable?) {
self.code = code
self.message = message
self.details = details
}
var localizedDescription: String {
return
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
}
}
private func wrapResult(_ result: Any?) -> [Any?] {
return [result]
... ... @@ -96,7 +113,7 @@ func deepHashHealthKitApi(value: Any?, hasher: inout Hasher) {
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
for key in valueDict.keys {
hasher.combine(key)
deepHashHealthKitApi(value: valueDict[key]!, hasher: &hasher)
}
... ... @@ -110,7 +127,7 @@ func deepHashHealthKitApi(value: Any?, hasher: inout Hasher) {
return hasher.combine(String(describing: value))
}
/// Generated class from Pigeon that represents data sent in messages.
struct HealthAuthorization: Hashable {
... ... @@ -142,11 +159,94 @@ struct HealthAuthorization: Hashable {
}
}
/// The user's current Huawei Health activity-ring goals.
///
/// Units: move is kcal, exercise is seconds, and stand is hours.
///
/// Generated class from Pigeon that represents data sent in messages.
struct HealthActivityGoal: Hashable {
var move: Int64? = nil
var step: Int64? = nil
var stand: Int64? = nil
var exercise: Int64? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> HealthActivityGoal? {
let move: Int64? = nilOrValue(pigeonVar_list[0])
let step: Int64? = nilOrValue(pigeonVar_list[1])
let stand: Int64? = nilOrValue(pigeonVar_list[2])
let exercise: Int64? = nilOrValue(pigeonVar_list[3])
return HealthActivityGoal(
move: move,
step: step,
stand: stand,
exercise: exercise
)
}
func toList() -> [Any?] {
return [
move,
step,
stand,
exercise,
]
}
static func == (lhs: HealthActivityGoal, rhs: HealthActivityGoal) -> Bool {
return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
func hash(into hasher: inout Hasher) {
deepHashHealthKitApi(value: toList(), hasher: &hasher)
}
}
/// One workout record read from Health Service Kit.
///
/// Generated class from Pigeon that represents data sent in messages.
struct HealthWorkoutDataPoint: Hashable {
/// Unix timestamp in seconds.
var startTime: Int64
/// Unix timestamp in seconds.
var endTime: Int64
/// The native Health Service Kit exercise-type identifier.
var activityType: Int64
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> HealthWorkoutDataPoint? {
let startTime = pigeonVar_list[0] as! Int64
let endTime = pigeonVar_list[1] as! Int64
let activityType = pigeonVar_list[2] as! Int64
return HealthWorkoutDataPoint(
startTime: startTime,
endTime: endTime,
activityType: activityType
)
}
func toList() -> [Any?] {
return [
startTime,
endTime,
activityType,
]
}
static func == (lhs: HealthWorkoutDataPoint, rhs: HealthWorkoutDataPoint) -> Bool {
return deepEqualsHealthKitApi(lhs.toList(), rhs.toList()) }
func hash(into hasher: inout Hasher) {
deepHashHealthKitApi(value: toList(), hasher: &hasher)
}
}
private class HealthKitApiPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
return HealthAuthorization.fromList(self.readValue() as! [Any?])
case 130:
return HealthActivityGoal.fromList(self.readValue() as! [Any?])
case 131:
return HealthWorkoutDataPoint.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
... ... @@ -158,6 +258,12 @@ private class HealthKitApiPigeonCodecWriter: FlutterStandardWriter {
if let value = value as? HealthAuthorization {
super.writeByte(129)
super.writeValue(value.toList())
} else if let value = value as? HealthActivityGoal {
super.writeByte(130)
super.writeValue(value.toList())
} else if let value = value as? HealthWorkoutDataPoint {
super.writeByte(131)
super.writeValue(value.toList())
} else {
super.writeValue(value)
}
... ... @@ -189,6 +295,10 @@ protocol HealthKitHostApi {
/// This only triggers the Health app's configured device-to-cloud sync; it
/// does not wait for, or return, the subsequently available cloud data.
func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void)
/// Reads the current activity-ring goals from Huawei Health on OHOS.
func readActivityGoal(completion: @escaping (Result<HealthActivityGoal?, Error>) -> Void)
/// Reads workout records in the supplied Unix-seconds time range on OHOS.
func readWorkoutData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthWorkoutDataPoint], Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
... ... @@ -247,5 +357,40 @@ class HealthKitHostApiSetup {
} else {
syncHealthDataToCloudChannel.setMessageHandler(nil)
}
/// Reads the current activity-ring goals from Huawei Health on OHOS.
let readActivityGoalChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
readActivityGoalChannel.setMessageHandler { _, reply in
api.readActivityGoal { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
readActivityGoalChannel.setMessageHandler(nil)
}
/// Reads workout records in the supplied Unix-seconds time range on OHOS.
let readWorkoutDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
readWorkoutDataChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let startTimeArg = args[0] as! Int64
let endTimeArg = args[1] as! Int64
api.readWorkoutData(startTime: startTimeArg, endTime: endTimeArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
readWorkoutDataChannel.setMessageHandler(nil)
}
}
}
... ...
... ... @@ -56,4 +56,14 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
// Huawei Health manual cloud synchronization is HarmonyOS-specific.
completion(.success(false))
}
func readActivityGoal(completion: @escaping (Result<HealthActivityGoal?, Error>) -> Void) {
// Activity-ring goals are read through Health Service Kit on OHOS only.
completion(.success(nil))
}
func readWorkoutData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthWorkoutDataPoint], Error>) -> Void) {
// Workout records are read through Health Service Kit on OHOS only.
completion(.success([]))
}
}
... ...
... ... @@ -223,6 +223,20 @@ class AppHealthKitHostApi extends _AppPigeonApiFacade {
final health_kit.HealthKitHostApi _api;
Future<health_kit.HealthActivityGoal?> readActivityGoal() => dispatch(
ios: () async => null,
ohos: () => _api.readActivityGoal(),
);
Future<List<health_kit.HealthWorkoutDataPoint>> readWorkoutData(
int startTime,
int endTime,
) =>
dispatch(
ios: () async => const <health_kit.HealthWorkoutDataPoint>[],
ohos: () => _api.readWorkoutData(startTime, endTime),
);
Future<health_kit.HealthAuthorization> checkHealthAppAuthorization() =>
dispatch(
ios: () => _api.checkHealthAppAuthorization(),
... ...
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
... ... @@ -10,6 +12,13 @@ import 'huawei_health_data_type.dart';
import 'ohos_health_raw_data_local_store.dart';
import 'ohos_health_raw_data_sync_service.dart';
/// Flutter-side switch for selecting the OHOS health data source at runtime.
///
/// Set [preferNativeHealthData] to true to read Health Service Kit first.
abstract final class OhosHealthDataSourceSwitch {
static bool preferNativeHealthData = false;
}
OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({
int Function()? userIdProvider,
}) {
... ... @@ -28,9 +37,13 @@ OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({
}
class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
const HarmonyApiOhosRawDataClient(this._harmonyApi);
const HarmonyApiOhosRawDataClient(this._harmonyApi, [this._healthKitApi]);
final HarmonyApi _harmonyApi;
final AppHealthKitHostApi? _healthKitApi;
bool get _preferNativeHealthData =>
OhosHealthDataSourceSwitch.preferNativeHealthData;
@override
Future<AppResult<HmHealthData>> getHealthData(
... ... @@ -47,12 +60,130 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
}
@override
Future<AppResult<HmWorkoutData>> getWorkoutData(int startTime, int endTime) {
return _harmonyApi.getWorkoutData(startTime, endTime);
Future<AppResult<HmWorkoutData>> getWorkoutData(
int startTime,
int endTime,
) async {
if (!_preferNativeHealthData) {
AppLogger.i(
'[OHOS_WORKOUT] source=remote native_enabled=false '
'range=$startTime-$endTime',
);
return _harmonyApi.getWorkoutData(startTime, endTime);
}
List<HmWorkoutDataItem> nativeItems = const <HmWorkoutDataItem>[];
try {
final records = await (_healthKitApi ?? AppHealthKitHostApi())
.readWorkoutData(startTime, endTime);
nativeItems = records
.map(
(record) => HmWorkoutDataItem(
startTime: record.startTime,
endTime: record.endTime,
activityType: record.activityType,
),
)
.toList(growable: false);
} catch (error) {
AppLogger.i('[OHOS_WORKOUT] native_failed error=$error');
}
if (nativeItems.isNotEmpty) {
AppLogger.i(
'[OHOS_WORKOUT] source=native '
'range=$startTime-$endTime '
'value=${_workoutLogValue(nativeItems)}',
);
return AppSuccess(HmWorkoutData(list: nativeItems));
}
final remoteResult = await _harmonyApi.getWorkoutData(startTime, endTime);
switch (remoteResult) {
case AppSuccess(:final data):
AppLogger.i(
'[OHOS_WORKOUT] '
'range=$startTime-$endTime '
'source=remote value=${_workoutLogValue(data.list ?? const <HmWorkoutDataItem>[])}',
);
case AppFailure(:final error):
AppLogger.i(
'[OHOS_WORKOUT] '
'range=$startTime-$endTime source=remote_failed error=$error',
);
}
return remoteResult;
}
@override
Future<AppResult<V2ActivityTarget>> getActivityGoal() {
return _harmonyApi.getActivityGoal();
Future<AppResult<V2ActivityTarget>> getActivityGoal() async {
if (!_preferNativeHealthData) {
AppLogger.i('[OHOS_ACTIVITY_GOAL] source=remote native_enabled=false');
return _harmonyApi.getActivityGoal();
}
try {
final goal =
await (_healthKitApi ?? AppHealthKitHostApi()).readActivityGoal();
if (goal != null) {
final nativeGoal = V2ActivityTarget(
move: goal.move,
step: goal.step,
stand: goal.stand,
exercise: goal.exercise,
);
if (_hasGoal(nativeGoal)) {
AppLogger.i(
'[OHOS_ACTIVITY_GOAL] '
'source=native value=${_goalLogValue(nativeGoal)}',
);
return AppSuccess(nativeGoal);
}
}
} catch (error) {
// Health Service Kit may be unavailable or not authorized. Keep the
// existing server endpoint as the compatibility fallback.
AppLogger.i('[OHOS_ACTIVITY_GOAL] native_failed error=$error');
}
final remoteResult = await _harmonyApi.getActivityGoal();
switch (remoteResult) {
case AppSuccess(:final data):
AppLogger.i(
'[OHOS_ACTIVITY_GOAL] '
'source=remote value=${_goalLogValue(data)}',
);
case AppFailure(:final error):
AppLogger.i(
'[OHOS_ACTIVITY_GOAL] source=remote_failed error=$error',
);
}
return remoteResult;
}
static bool _hasGoal(V2ActivityTarget goal) {
return goal.move != null ||
goal.step != null ||
goal.stand != null ||
goal.exercise != null;
}
static String _goalLogValue(V2ActivityTarget? goal) {
if (goal == null) return 'null';
return 'move=${goal.move},step=${goal.step},stand=${goal.stand},exercise=${goal.exercise}';
}
static String _workoutLogValue(List<HmWorkoutDataItem> items) {
const logLimit = 100;
final values = items
.take(logLimit)
.map(
(item) => '${item.startTime}-${item.endTime}-${item.activityType}',
)
.join(',');
final suffix = items.length > logLimit ? ',…' : '';
return 'count=${items.length} [$values$suffix]';
}
}
... ...
... ... @@ -77,6 +77,120 @@ class HealthAuthorization {
;
}
/// The user's current Huawei Health activity-ring goals.
///
/// Units: move is kcal, exercise is seconds, and stand is hours.
class HealthActivityGoal {
HealthActivityGoal({
this.move,
this.step,
this.stand,
this.exercise,
});
int? move;
int? step;
int? stand;
int? exercise;
List<Object?> _toList() {
return <Object?>[
move,
step,
stand,
exercise,
];
}
Object encode() {
return _toList(); }
static HealthActivityGoal decode(Object result) {
result as List<Object?>;
return HealthActivityGoal(
move: result[0] as int?,
step: result[1] as int?,
stand: result[2] as int?,
exercise: result[3] as int?,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! HealthActivityGoal || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList())
;
}
/// One workout record read from Health Service Kit.
class HealthWorkoutDataPoint {
HealthWorkoutDataPoint({
required this.startTime,
required this.endTime,
required this.activityType,
});
/// Unix timestamp in seconds.
int startTime;
/// Unix timestamp in seconds.
int endTime;
/// The native Health Service Kit exercise-type identifier.
int activityType;
List<Object?> _toList() {
return <Object?>[
startTime,
endTime,
activityType,
];
}
Object encode() {
return _toList(); }
static HealthWorkoutDataPoint decode(Object result) {
result as List<Object?>;
return HealthWorkoutDataPoint(
startTime: result[0]! as int,
endTime: result[1]! as int,
activityType: result[2]! as int,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! HealthWorkoutDataPoint || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList())
;
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
... ... @@ -88,6 +202,12 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is HealthAuthorization) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is HealthActivityGoal) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is HealthWorkoutDataPoint) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
... ... @@ -96,8 +216,12 @@ class _PigeonCodec extends StandardMessageCodec {
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
case 129:
return HealthAuthorization.decode(readValue(buffer)!);
case 130:
return HealthActivityGoal.decode(readValue(buffer)!);
case 131:
return HealthWorkoutDataPoint.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
... ... @@ -205,4 +329,57 @@ class HealthKitHostApi {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// Reads the current activity-ring goals from Huawei Health on OHOS.
Future<HealthActivityGoal?> readActivityGoal() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return (pigeonVar_replyList[0] as HealthActivityGoal?);
}
}
/// Reads workout records in the supplied Unix-seconds time range on OHOS.
Future<List<HealthWorkoutDataPoint>> readWorkoutData(int startTime, int endTime) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[startTime, endTime]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<HealthWorkoutDataPoint>();
}
}
}
... ...
import { HealthAuthorization, HealthKitHostApi, Result } from '../pigeon/HealthKitApi';
import { HealthActivityGoal, HealthAuthorization, HealthKitHostApi, HealthWorkoutDataPoint, Result } from '../pigeon/HealthKitApi';
import common from '@ohos.app.ability.common';
import { bundleManager, Want } from '@kit.AbilityKit';
import { productViewManager } from '@kit.AppGalleryKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { healthStore } from '@kit.HealthServiceKit';
import { healthService, healthStore } from '@kit.HealthServiceKit';
/**
* Bridges Flutter's health authorization flow to Health Service Kit.
... ... @@ -91,11 +91,62 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
});
}
readActivityGoal(result: Result<HealthActivityGoal | undefined>): void {
this.ensureInitialized()
.then(() => healthService.workout.readActivityReport())
.then((report: healthService.workout.ActivityReport) => {
result.success(new HealthActivityGoal(
this.activeCaloriesToMoveGoal(report.activeCaloriesGoal),
report.stepsGoal,
report.activeHoursGoal,
this.exerciseMinutesToSeconds(report.exerciseGoal),
));
})
.catch((error: Error) => {
console.error(`readActivityGoal failed: ${error.name}: ${error.message}`);
result.error(error);
});
}
readWorkoutData(
startTime: number,
endTime: number,
result: Result<Array<HealthWorkoutDataPoint>>,
): void {
this.ensureInitialized()
.then(() => healthStore.readData<healthStore.ExerciseSequence>({
startTime: startTime * 1000,
endTime: endTime * 1000,
exerciseType: null,
}))
.then((records: Array<healthStore.ExerciseSequence>) => {
result.success(records.map((record: healthStore.ExerciseSequence) => {
return new HealthWorkoutDataPoint(
Math.floor(record.startTime / 1000),
Math.floor(record.endTime / 1000),
record.exerciseType.id,
);
}));
})
.catch((error: Error) => {
console.error(`readWorkoutData failed: ${error.name}: ${error.message}`);
result.error(error);
});
}
private async getAuthorization(): Promise<healthStore.AuthorizationResponse> {
await this.ensureInitialized();
return healthStore.getAuthorizations(this.authorizationRequest);
}
private exerciseMinutesToSeconds(value: number | undefined): number | undefined {
return value === undefined ? undefined : Math.round(value * 60);
}
private activeCaloriesToMoveGoal(value: number | undefined): number | undefined {
return value === undefined ? undefined : Math.floor(value / 1000);
}
private async requestAuthorizations(): Promise<healthStore.AuthorizationResponse> {
await this.ensureInitialized();
return healthStore.requestAuthorizations(this.context, this.authorizationRequest);
... ...
... ... @@ -96,6 +96,184 @@ export class HealthAuthorization {
}
}
/*
* The user's current Huawei Health activity-ring goals.
*
* Units: move is kcal, exercise is seconds, and stand is hours.
*
* Generated class from Pigeon that represents data sent in messages.
*/
export class HealthActivityGoal {
private move?: number;
public setMove(move: number | undefined): void {
this.move = move;
}
getMove(): number | undefined {
return this.move;
}
private step?: number;
public setStep(step: number | undefined): void {
this.step = step;
}
getStep(): number | undefined {
return this.step;
}
private stand?: number;
public setStand(stand: number | undefined): void {
this.stand = stand;
}
getStand(): number | undefined {
return this.stand;
}
private exercise?: number;
public setExercise(exercise: number | undefined): void {
this.exercise = exercise;
}
getExercise(): number | undefined {
return this.exercise;
}
constructor(move?: number, step?: number, stand?: number, exercise?: number) {
this.move = move;
this.step = step;
this.stand = stand;
this.exercise = exercise;
}
toList(): Array<Object | null> {
let arr: Array<Object | null> = new Array<Object | null>();
if (this.move === undefined || this.move === null) {
arr.push(null);
} else {
arr.push(this.move);
}
if (this.step === undefined || this.step === null) {
arr.push(null);
} else {
arr.push(this.step);
}
if (this.stand === undefined || this.stand === null) {
arr.push(null);
} else {
arr.push(this.stand);
}
if (this.exercise === undefined || this.exercise === null) {
arr.push(null);
} else {
arr.push(this.exercise);
}
return arr;
}
static fromList(arr: Object[]): HealthActivityGoal {
let move: number | undefined = undefined;
if (arr[0] !== null && arr[0] !== undefined) {
let moveObject: Object = arr[0];
move = moveObject as number | undefined;
}
let step: number | undefined = undefined;
if (arr[1] !== null && arr[1] !== undefined) {
let stepObject: Object = arr[1];
step = stepObject as number | undefined;
}
let stand: number | undefined = undefined;
if (arr[2] !== null && arr[2] !== undefined) {
let standObject: Object = arr[2];
stand = standObject as number | undefined;
}
let exercise: number | undefined = undefined;
if (arr[3] !== null && arr[3] !== undefined) {
let exerciseObject: Object = arr[3];
exercise = exerciseObject as number | undefined;
}
return new HealthActivityGoal(move, step, stand, exercise);
}
}
/*
* One workout record read from Health Service Kit.
*
* Generated class from Pigeon that represents data sent in messages.
*/
export class HealthWorkoutDataPoint {
private startTime: number;
public setStartTime(startTime: number): void {
this.startTime = startTime;
}
getStartTime(): number {
return this.startTime;
}
private endTime: number;
public setEndTime(endTime: number): void {
this.endTime = endTime;
}
getEndTime(): number {
return this.endTime;
}
private activityType: number;
public setActivityType(activityType: number): void {
this.activityType = activityType;
}
getActivityType(): number {
return this.activityType;
}
constructor(startTime: number, endTime: number, activityType: number) {
this.startTime = startTime;
this.endTime = endTime;
this.activityType = activityType;
}
toList(): Array<Object | null> {
let arr: Array<Object | null> = new Array<Object | null>();
if (this.startTime === undefined || this.startTime === null) {
arr.push(null);
} else {
arr.push(this.startTime);
}
if (this.endTime === undefined || this.endTime === null) {
arr.push(null);
} else {
arr.push(this.endTime);
}
if (this.activityType === undefined || this.activityType === null) {
arr.push(null);
} else {
arr.push(this.activityType);
}
return arr;
}
static fromList(arr: Object[]): HealthWorkoutDataPoint {
let startTimeObject: Object = arr[0];
const startTime: number = startTimeObject as number;
let endTimeObject: Object = arr[1];
const endTime: number = endTimeObject as number;
let activityTypeObject: Object = arr[2];
const activityType: number = activityTypeObject as number;
return new HealthWorkoutDataPoint(startTime, endTime, activityType);
}
}
export class PigeonCodec extends StandardMessageCodec {
static readonly INSTANCE: PigeonCodec = new PigeonCodec();
... ... @@ -112,6 +290,10 @@ export class PigeonCodec extends StandardMessageCodec {
switch (type) {
case this.getByte(129):
return HealthAuthorization.fromList(super.readValue(buffer) as Object[]);
case this.getByte(130):
return HealthActivityGoal.fromList(super.readValue(buffer) as Object[]);
case this.getByte(131):
return HealthWorkoutDataPoint.fromList(super.readValue(buffer) as Object[]);
default:
return super.readValueOfType(type, buffer);
}
... ... @@ -121,6 +303,12 @@ export class PigeonCodec extends StandardMessageCodec {
if (value instanceof HealthAuthorization) {
stream.writeUint8(this.getByte(129));
this.writeValue(stream, (value as HealthAuthorization).toList());
} else if (value instanceof HealthActivityGoal) {
stream.writeUint8(this.getByte(130));
this.writeValue(stream, (value as HealthActivityGoal).toList());
} else if (value instanceof HealthWorkoutDataPoint) {
stream.writeUint8(this.getByte(131));
this.writeValue(stream, (value as HealthWorkoutDataPoint).toList());
} else {
super.writeValue(stream, value);
}
... ... @@ -146,6 +334,10 @@ export abstract class HealthKitHostApi {
* does not wait for, or return, the subsequently available cloud data.
*/
abstract syncHealthDataToCloud(result: Result<boolean>): void;
/* Reads the current activity-ring goals from Huawei Health on OHOS.*/
abstract readActivityGoal(result: Result<HealthActivityGoal | undefined>): void;
/* Reads workout records in the supplied Unix-seconds time range on OHOS.*/
abstract readWorkoutData(startTime: number, endTime: number, result: Result<Array<HealthWorkoutDataPoint>>): void;
/** The codec used by HealthKitHostApi. */
static getCodec(): MessageCodec<Object> {
return PigeonCodec.INSTANCE;
... ... @@ -234,5 +426,68 @@ export abstract class HealthKitHostApi {
channel.setMessageHandler(null);
}
}
{
let channel: BasicMessageChannel<Object> =
new BasicMessageChannel(
binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readActivityGoal' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
if (api != null) {
channel.setMessageHandler({
onMessage(message: Object, reply: Reply<Object>) {
class ResultImp implements Result<HealthActivityGoal | undefined>{
success(result: HealthActivityGoal | undefined): void {
let res: Array<Object | null> = [];
res.push(result);
reply.reply(res);
}
error(error: Error): void {
let wrappedError: Array<Object | null> = wrapError(error);
reply.reply(wrappedError);
}
}
let resultCallback: Result<HealthActivityGoal | undefined> = new ResultImp();
api!.readActivityGoal(resultCallback);
} });
} else {
channel.setMessageHandler(null);
}
}
{
let channel: BasicMessageChannel<Object> =
new BasicMessageChannel(
binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.readWorkoutData' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
if (api != null) {
channel.setMessageHandler({
onMessage(message: Object, reply: Reply<Object>) {
if (!(Array.isArray(message))) {
reply.reply(wrapError(new Error('Invalid Pigeon message: expected List.')));
return;
}
let args: Array<Object> = message as Array<Object>;
if (args.length < 2) {
reply.reply(wrapError(new Error('Invalid Pigeon message: expected at least 2 argument(s).')));
return;
}
class ResultImp implements Result<Array<HealthWorkoutDataPoint>>{
success(result: Array<HealthWorkoutDataPoint>): void {
let res: Array<Object | null> = [];
res.push(result);
reply.reply(res);
}
error(error: Error): void {
let wrappedError: Array<Object | null> = wrapError(error);
reply.reply(wrappedError);
}
}
let resultCallback: Result<Array<HealthWorkoutDataPoint>> = new ResultImp();
api!.readWorkoutData(args[0] as number, args[1] as number, resultCallback);
} });
} else {
channel.setMessageHandler(null);
}
}
}
}
... ...
... ... @@ -8,6 +8,41 @@ class HealthAuthorization {
HealthAuthorization({required this.status, required this.hasData});
}
/// The user's current Huawei Health activity-ring goals.
///
/// Units: move is kcal, exercise is seconds, and stand is hours.
class HealthActivityGoal {
HealthActivityGoal({
this.move,
this.step,
this.stand,
this.exercise,
});
int? move;
int? step;
int? stand;
int? exercise;
}
/// One workout record read from Health Service Kit.
class HealthWorkoutDataPoint {
HealthWorkoutDataPoint({
required this.startTime,
required this.endTime,
required this.activityType,
});
/// Unix timestamp in seconds.
int startTime;
/// Unix timestamp in seconds.
int endTime;
/// The native Health Service Kit exercise-type identifier.
int activityType;
}
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/pigeon/health_kit_api.g.dart',
... ... @@ -39,4 +74,12 @@ abstract class HealthKitHostApi {
/// does not wait for, or return, the subsequently available cloud data.
@async
bool syncHealthDataToCloud();
/// Reads the current activity-ring goals from Huawei Health on OHOS.
@async
HealthActivityGoal? readActivityGoal();
/// Reads workout records in the supplied Unix-seconds time range on OHOS.
@async
List<HealthWorkoutDataPoint> readWorkoutData(int startTime, int endTime);
}
... ...